797 lines
26 KiB
Bash
797 lines
26 KiB
Bash
#!/bin/sh
|
|
# shellcheck shell=dash
|
|
# shellcheck disable=SC3043 # local is non-POSIX but supported by all target shells
|
|
# Firefox installer script
|
|
# Usage: curl --proto '=https' --tlsv1.2 -sSf <url>/install-firefox.sh -o install-firefox.sh && sh install-firefox.sh [OPTIONS]
|
|
# or: sh install-firefox.sh [OPTIONS]
|
|
#
|
|
# Options:
|
|
# --channel <release|beta|devedition|nightly> Choose Firefox channel (default: release)
|
|
# --install-method <method> Force install method: apt, rpm, flatpak, tarball, snap
|
|
# --lang <locale> Force locale (e.g. fr, de, ja)
|
|
# -v, --verbose Enable verbose output
|
|
# -h, --help Show this help
|
|
|
|
set -eu
|
|
|
|
CHANNEL="release"
|
|
INSTALL_METHOD=""
|
|
LANG_OVERRIDE=""
|
|
VERBOSE=false
|
|
ARCH=""
|
|
DISTRO=""
|
|
DISTRO_VERSION=""
|
|
DETECTED_LOCALE=""
|
|
|
|
info() { printf "[info] %s\n" "$*"; }
|
|
ok() { printf "[ok] %s\n" "$*"; }
|
|
warn() { printf "[warn] %s\n" "$*" >&2; }
|
|
error() { printf "[error] %s\n" "$*" >&2; exit 1; }
|
|
|
|
need_cmd() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
error "Required command not found: $1"
|
|
fi
|
|
}
|
|
|
|
check_cmd() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
run_sudo() {
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
"$@"
|
|
else
|
|
[ "$VERBOSE" = true ] && info "Running with sudo: $*"
|
|
sudo "$@"
|
|
fi
|
|
}
|
|
|
|
retry() {
|
|
local _n=3 _delay=5 _i=0
|
|
while true; do
|
|
if "$@"; then
|
|
return 0
|
|
fi
|
|
_i=$((_i + 1))
|
|
if [ "$_i" -ge "$_n" ]; then
|
|
error "Command failed after $_n attempts: $*"
|
|
fi
|
|
warn "Attempt $_i/$_n failed; retrying in ${_delay}s..."
|
|
sleep "$_delay"
|
|
_delay=$((_delay * 2))
|
|
done
|
|
}
|
|
|
|
# Finds the l10n package for $DETECTED_LOCALE given a base package name and a
|
|
# package existence check command. Tries the full locale code first (fr-FR),
|
|
# then the language-only code (fr). Sets _l10n_pkg to the found name or "".
|
|
find_l10n_package() {
|
|
local _check_cmd="$1"
|
|
local _base_pkg="$2"
|
|
_l10n_pkg=""
|
|
if [ "$DETECTED_LOCALE" = "en-US" ]; then
|
|
return
|
|
fi
|
|
local _full _lang _cand
|
|
_full=$(echo "$DETECTED_LOCALE" | tr '[:upper:]_' '[:lower:]-')
|
|
_lang="${_full%%-*}"
|
|
for _cand in "$_full" "$_lang"; do
|
|
if "$_check_cmd" "${_base_pkg}-l10n-${_cand}" >/dev/null 2>&1; then
|
|
_l10n_pkg="${_base_pkg}-l10n-${_cand}"
|
|
return
|
|
fi
|
|
done
|
|
warn "No language pack available for $DETECTED_LOCALE; installing en-US"
|
|
}
|
|
|
|
# Downloader abstraction: prefer curl, fall back to wget
|
|
download() {
|
|
local _url="$1"
|
|
local _output="$2"
|
|
|
|
if check_cmd curl; then
|
|
curl --proto '=https' --tlsv1.2 -sSf --retry 3 -C - -L -o "$_output" "$_url"
|
|
elif check_cmd wget; then
|
|
wget --https-only --quiet --continue -O "$_output" "$_url"
|
|
else
|
|
error "Neither curl nor wget found. Install one and retry."
|
|
fi
|
|
}
|
|
|
|
# Downloader for piping to stdout (e.g. GPG keys)
|
|
download_to_stdout() {
|
|
local _url="$1"
|
|
|
|
if check_cmd curl; then
|
|
curl --proto '=https' --tlsv1.2 -sSf --retry 3 "$_url"
|
|
elif check_cmd wget; then
|
|
wget --https-only --quiet -O - "$_url"
|
|
else
|
|
error "Neither curl nor wget found. Install one and retry."
|
|
fi
|
|
}
|
|
|
|
rpm_pkg_exists() {
|
|
local _mgr="$1"
|
|
local _name="$2"
|
|
case "$_mgr" in
|
|
dnf|yum) "$_mgr" info "$_name" >/dev/null 2>&1 ;;
|
|
zypper) zypper --non-interactive search --match-exact --type package "$_name" >/dev/null 2>&1 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Check that a URL is reachable before relying on it
|
|
check_url() {
|
|
local _url="$1"
|
|
|
|
if check_cmd curl; then
|
|
curl --proto '=https' --tlsv1.2 -sSf --head --retry 2 "$_url" >/dev/null 2>&1
|
|
elif check_cmd wget; then
|
|
wget --https-only --spider --quiet "$_url" 2>/dev/null
|
|
else
|
|
# Unreachable in practice: main() checks for curl/wget before calling
|
|
# any install function that invokes check_url.
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Firefox installer script
|
|
Usage: curl --proto '=https' --tlsv1.2 -sSf <url>/install-firefox.sh -o install-firefox.sh && sh install-firefox.sh [OPTIONS]
|
|
or: sh install-firefox.sh [OPTIONS]
|
|
|
|
Options:
|
|
--channel <release|beta|devedition|nightly> Choose Firefox channel (default: release)
|
|
--install-method <method> Force install method: apt, rpm, flatpak, tarball, snap
|
|
--lang <locale> Force locale (e.g. fr, de, ja)
|
|
-v, --verbose Enable verbose output (shell trace + sudo command logging)
|
|
-h, --help Show this help
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
parse_args() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--channel)
|
|
shift
|
|
CHANNEL="${1:?Missing channel value}"
|
|
case "$CHANNEL" in
|
|
release|beta|devedition|nightly) ;;
|
|
*) error "Invalid channel '$CHANNEL'. Must be: release, beta, devedition, nightly" ;;
|
|
esac
|
|
;;
|
|
--install-method)
|
|
shift
|
|
INSTALL_METHOD="${1:?Missing install method value}"
|
|
case "$INSTALL_METHOD" in
|
|
apt|rpm|flatpak|snap|tarball) ;;
|
|
*) error "Invalid install method '$INSTALL_METHOD'. Must be: apt, rpm, flatpak, snap, tarball" ;;
|
|
esac
|
|
;;
|
|
--lang)
|
|
shift
|
|
LANG_OVERRIDE="${1:?Missing lang value}"
|
|
;;
|
|
-v|--verbose)
|
|
VERBOSE=true
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
;;
|
|
*)
|
|
error "Unknown option: $1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
detect_os() {
|
|
need_cmd uname
|
|
|
|
case "$(uname -s)" in
|
|
Linux) ;;
|
|
*)
|
|
warn "Diagnostic info for bug report:"
|
|
warn " uname -a: $(uname -a)"
|
|
[ -f /etc/os-release ] && warn " os-release: $(cat /etc/os-release)"
|
|
error "Unsupported operating system: $(uname -s) (this script only supports Linux)"
|
|
;;
|
|
esac
|
|
|
|
case "$(uname -m)" in
|
|
x86_64|amd64) ARCH="x86_64" ;;
|
|
aarch64|arm64) ARCH="aarch64" ;;
|
|
*)
|
|
warn "Diagnostic info for bug report:"
|
|
warn " uname -a: $(uname -a)"
|
|
error "Unsupported architecture: $(uname -m)"
|
|
;;
|
|
esac
|
|
|
|
local _os_release="${_OS_RELEASE:-/etc/os-release}"
|
|
if [ -f "$_os_release" ]; then
|
|
# shellcheck source=/dev/null
|
|
. "$_os_release"
|
|
DISTRO="${ID:-unknown}"
|
|
DISTRO_VERSION="${VERSION_ID:-}"
|
|
info "Detected: ${PRETTY_NAME:-$DISTRO} ($ARCH)"
|
|
else
|
|
DISTRO="unknown"
|
|
warn "Could not detect Linux distribution"
|
|
fi
|
|
}
|
|
|
|
detect_locale() {
|
|
if [ -n "$LANG_OVERRIDE" ]; then
|
|
DETECTED_LOCALE="$LANG_OVERRIDE"
|
|
info "Using forced locale: $DETECTED_LOCALE"
|
|
return
|
|
fi
|
|
|
|
local _raw_locale=""
|
|
_raw_locale="${LC_ALL:-${LC_MESSAGES:-${LANG:-en_US.UTF-8}}}"
|
|
_raw_locale="${_raw_locale%%.*}" # strip .UTF-8
|
|
_raw_locale="${_raw_locale%%@*}" # strip @modifier
|
|
|
|
case "$_raw_locale" in
|
|
C|POSIX|"") DETECTED_LOCALE="en-US" ;;
|
|
*_*)
|
|
local _lang="${_raw_locale%%_*}"
|
|
local _region="${_raw_locale##*_}"
|
|
DETECTED_LOCALE="${_lang}-${_region}"
|
|
;;
|
|
*)
|
|
DETECTED_LOCALE="$_raw_locale"
|
|
;;
|
|
esac
|
|
info "Detected locale: $DETECTED_LOCALE"
|
|
}
|
|
|
|
channel_to_package() {
|
|
case "$CHANNEL" in
|
|
release) echo "firefox" ;;
|
|
beta) echo "firefox-beta" ;;
|
|
devedition) echo "firefox-devedition" ;;
|
|
nightly) echo "firefox-nightly" ;;
|
|
*) error "Unsupported channel: $CHANNEL" ;;
|
|
esac
|
|
}
|
|
|
|
apt_source_format() {
|
|
case "$DISTRO" in
|
|
debian)
|
|
if [ -n "$DISTRO_VERSION" ] && \
|
|
dpkg --compare-versions "$DISTRO_VERSION" ge "13" 2>/dev/null; then
|
|
echo "deb822"; return
|
|
fi
|
|
;;
|
|
ubuntu)
|
|
if [ -n "$DISTRO_VERSION" ] && \
|
|
dpkg --compare-versions "$DISTRO_VERSION" ge "25.10" 2>/dev/null; then
|
|
echo "deb822"; return
|
|
fi
|
|
;;
|
|
esac
|
|
echo "list"
|
|
}
|
|
|
|
# Returns 0 if the system supports the modern signed-by keyring (Debian 11+,
|
|
# Ubuntu 22.04+), 1 if it requires the legacy apt-key path.
|
|
apt_use_modern_keyring() {
|
|
case "$DISTRO" in
|
|
debian)
|
|
if [ -n "$DISTRO_VERSION" ] && \
|
|
dpkg --compare-versions "$DISTRO_VERSION" lt "11" 2>/dev/null; then
|
|
return 1
|
|
fi
|
|
;;
|
|
ubuntu)
|
|
if [ -n "$DISTRO_VERSION" ] && \
|
|
dpkg --compare-versions "$DISTRO_VERSION" lt "22.04" 2>/dev/null; then
|
|
return 1
|
|
fi
|
|
;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
apt_verify_gpg_fingerprint() {
|
|
local _keyring="$1"
|
|
local _expected="35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3"
|
|
local _actual
|
|
_actual="$(gpg --with-colons --show-keys "$_keyring" 2>/dev/null \
|
|
| awk -F: '/^fpr:/{print $10; exit}')"
|
|
if [ "$_actual" != "$_expected" ]; then
|
|
run_sudo rm -f "$_keyring"
|
|
error "GPG key fingerprint mismatch: expected $_expected, got $_actual"
|
|
fi
|
|
info "GPG key fingerprint verified: $_actual"
|
|
}
|
|
|
|
apt_write_source_entry() {
|
|
local _keyring="$1"
|
|
local _repo_url="$2"
|
|
if [ "$(apt_source_format)" = "deb822" ]; then
|
|
# ${_repo_url} and ${_keyring} are interpolated — safe because both are
|
|
# hardcoded Mozilla URLs set by the caller, never user-controlled input.
|
|
run_sudo tee /etc/apt/sources.list.d/mozilla.sources >/dev/null <<SRCEOF
|
|
Types: deb
|
|
URIs: ${_repo_url}
|
|
Suites: mozilla
|
|
Components: main
|
|
Signed-By: ${_keyring}
|
|
SRCEOF
|
|
else
|
|
echo "deb [signed-by=$_keyring] $_repo_url mozilla main" \
|
|
| run_sudo tee /etc/apt/sources.list.d/mozilla.list >/dev/null
|
|
fi
|
|
}
|
|
|
|
# Removes any conflicting legacy Mozilla apt sources that would prevent the
|
|
# modern signed-by keyring from working correctly.
|
|
# Path variables are overrideable for testing.
|
|
apt_cleanup_legacy_sources() {
|
|
local _legacy_key="${_APT_LEGACY_KEY:-/etc/apt/trusted.gpg.d/packages.mozilla.org.gpg}"
|
|
local _sources_dir="${_APT_SOURCES_DIR:-/etc/apt/sources.list.d}"
|
|
local _sources_list="${_APT_SOURCES_LIST:-/etc/apt/sources.list}"
|
|
if [ -e "$_legacy_key" ]; then
|
|
info "Removing legacy Mozilla keyring: $_legacy_key"
|
|
run_sudo rm -f "$_legacy_key"
|
|
fi
|
|
local _f
|
|
for _f in "$_sources_dir"/*.list "$_sources_dir"/*.sources; do
|
|
[ -e "$_f" ] || continue
|
|
if grep -q 'packages\.mozilla\.org' "$_f" 2>/dev/null; then
|
|
info "Removing existing Mozilla source: $_f"
|
|
run_sudo rm -f "$_f"
|
|
fi
|
|
done
|
|
if [ -e "$_sources_list" ] \
|
|
&& grep -q 'packages\.mozilla\.org' "$_sources_list" 2>/dev/null; then
|
|
info "Stripping Mozilla entries from $_sources_list"
|
|
run_sudo sed -i '/packages\.mozilla\.org/d' "$_sources_list"
|
|
fi
|
|
}
|
|
|
|
apt_setup_modern_keyring() {
|
|
local _key_url="$1"
|
|
local _repo_url="$2"
|
|
apt_cleanup_legacy_sources
|
|
run_sudo install -d -m 0755 /etc/apt/keyrings
|
|
local _keyring="/etc/apt/keyrings/packages.mozilla.org.asc"
|
|
if [ ! -f "$_keyring" ]; then
|
|
local _tmpkey
|
|
_tmpkey="$(mktemp)"
|
|
download "$_key_url" "$_tmpkey"
|
|
run_sudo install -m 644 "$_tmpkey" "$_keyring"
|
|
rm -f "$_tmpkey"
|
|
if check_cmd gpg; then
|
|
apt_verify_gpg_fingerprint "$_keyring"
|
|
else
|
|
warn "gpg not found, skipping key fingerprint verification"
|
|
fi
|
|
fi
|
|
apt_write_source_entry "$_keyring" "$_repo_url"
|
|
}
|
|
|
|
apt_setup_legacy_keyring() {
|
|
local _key_url="$1"
|
|
local _repo_url="$2"
|
|
info "Using legacy apt-key for older system"
|
|
local _tmpkey
|
|
_tmpkey="$(mktemp)"
|
|
download "$_key_url" "$_tmpkey"
|
|
if check_cmd gpg; then
|
|
apt_verify_gpg_fingerprint "$_tmpkey"
|
|
else
|
|
warn "gpg not found, skipping key fingerprint verification"
|
|
fi
|
|
run_sudo apt-key add "$_tmpkey"
|
|
rm -f "$_tmpkey"
|
|
echo "deb $_repo_url mozilla main" \
|
|
| run_sudo tee /etc/apt/sources.list.d/mozilla.list >/dev/null
|
|
}
|
|
|
|
apt_add_priority_pin() {
|
|
# Pin Mozilla repo higher than distro packages
|
|
run_sudo tee /etc/apt/preferences.d/mozilla >/dev/null <<'PINEOF'
|
|
Package: *
|
|
Pin: origin packages.mozilla.org
|
|
Pin-Priority: 1000
|
|
PINEOF
|
|
}
|
|
|
|
apt_install_packages() {
|
|
local _pkg="$1"
|
|
local _l10n_pkg
|
|
retry run_sudo apt-get update -qq
|
|
# Mozilla packages most languages without a region (firefox-l10n-fr),
|
|
# but some require it (firefox-l10n-pt-br, firefox-l10n-en-gb).
|
|
_apt_l10n_check() { apt-cache show "$1"; }
|
|
find_l10n_package _apt_l10n_check "$_pkg"
|
|
if [ -n "$_l10n_pkg" ]; then
|
|
retry run_sudo apt-get install -y "$_pkg" "$_l10n_pkg"
|
|
else
|
|
retry run_sudo apt-get install -y "$_pkg"
|
|
fi
|
|
}
|
|
|
|
install_apt() {
|
|
info "Installing Firefox ($CHANNEL) via apt..."
|
|
need_cmd apt-get
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
local _repo_url="https://packages.mozilla.org/apt"
|
|
local _key_url="https://packages.mozilla.org/apt/repo-signing-key.gpg"
|
|
if apt_use_modern_keyring; then
|
|
apt_setup_modern_keyring "$_key_url" "$_repo_url"
|
|
else
|
|
apt_setup_legacy_keyring "$_key_url" "$_repo_url"
|
|
fi
|
|
check_url "$_repo_url/dists/mozilla/Release" || \
|
|
error "Mozilla APT repository is not reachable at $_repo_url"
|
|
apt_add_priority_pin
|
|
apt_install_packages "$(channel_to_package)"
|
|
ok "Firefox ($CHANNEL, $ARCH, $DETECTED_LOCALE) installed via apt"
|
|
}
|
|
|
|
rpm_detect_pkg_manager() {
|
|
if check_cmd dnf; then
|
|
echo "dnf"
|
|
elif check_cmd yum; then
|
|
error "yum is not supported: verifying Mozilla's package signatures requires RPM 4.18+ (GPG subkey support), but yum-based systems ship older RPM versions that cannot verify the signing key. Use --install-method flatpak or --install-method tarball instead, or upgrade to a distribution with dnf (RHEL 8+, Fedora, etc.)."
|
|
elif check_cmd zypper; then
|
|
echo "zypper"
|
|
else
|
|
error "No supported RPM package manager found (dnf, yum, zypper)"
|
|
fi
|
|
}
|
|
|
|
rpm_add_repository() {
|
|
local _pkg_manager="$1"
|
|
local _repo_url="$2"
|
|
local _gpg_key="$3"
|
|
if [ "$_pkg_manager" = "zypper" ]; then
|
|
# --gpgcheck-allow-unsigned-repo: Mozilla's repo metadata is not GPG-signed;
|
|
# individual .rpm packages are verified via gpgcheck=1 at install time.
|
|
zypper lr mozilla-firefox >/dev/null 2>&1 || \
|
|
run_sudo zypper ar -f -p 10 --gpgcheck-allow-unsigned-repo \
|
|
"$_repo_url" mozilla-firefox
|
|
# Note: unlike the apt path, the downloaded key is not verified against a
|
|
# hardcoded fingerprint — packages are verified by gpgcheck=1 instead.
|
|
retry run_sudo zypper --gpg-auto-import-keys refresh mozilla-firefox
|
|
else
|
|
local _repo_file="/etc/yum.repos.d/mozilla-firefox.repo"
|
|
if [ ! -f "$_repo_file" ]; then
|
|
run_sudo tee "$_repo_file" >/dev/null <<REPOEOF
|
|
[mozilla-firefox]
|
|
name=Mozilla Firefox
|
|
baseurl=${_repo_url}
|
|
enabled=1
|
|
gpgcheck=1
|
|
repo_gpgcheck=0
|
|
gpgkey=${_gpg_key}
|
|
priority=10
|
|
REPOEOF
|
|
fi
|
|
fi
|
|
}
|
|
|
|
rpm_install_packages() {
|
|
local _pkg_manager="$1"
|
|
local _pkg="$2"
|
|
local _l10n_pkg="$3"
|
|
if [ "$_pkg_manager" = "zypper" ]; then
|
|
# --replacefiles: on openSUSE the distro ships MozillaFirefox which owns
|
|
# /usr/bin/firefox; zypper blocks the install without this flag.
|
|
retry run_sudo zypper install -y --replacefiles "$_pkg"
|
|
else
|
|
retry run_sudo "$_pkg_manager" install -y "$_pkg"
|
|
fi
|
|
if [ -n "$_l10n_pkg" ]; then
|
|
# Soft fallback: Mozilla RPM repos have fewer l10n packages than apt.
|
|
# One attempt only — retry would exit 1 on failure, defeating the fallback.
|
|
run_sudo "$_pkg_manager" install -y "$_l10n_pkg" 2>/dev/null || \
|
|
warn "Could not install language pack $_l10n_pkg; Firefox is installed without it"
|
|
fi
|
|
}
|
|
|
|
install_rpm() {
|
|
local _pkg_manager _pkg _repo_url _gpg_key _l10n_pkg
|
|
_pkg_manager="$(rpm_detect_pkg_manager)"
|
|
info "Installing Firefox ($CHANNEL) via $_pkg_manager..."
|
|
_pkg="$(channel_to_package)"
|
|
_repo_url="https://packages.mozilla.org/rpm/firefox"
|
|
_gpg_key="https://packages.mozilla.org/rpm/firefox/signing-key.gpg"
|
|
check_url "$_repo_url" || error "Mozilla RPM repository is not reachable at $_repo_url"
|
|
rpm_add_repository "$_pkg_manager" "$_repo_url" "$_gpg_key"
|
|
_rpm_l10n_check() { rpm_pkg_exists "$_pkg_manager" "$1"; }
|
|
find_l10n_package _rpm_l10n_check "$_pkg"
|
|
rpm_install_packages "$_pkg_manager" "$_pkg" "$_l10n_pkg"
|
|
ok "Firefox ($CHANNEL, $ARCH, $DETECTED_LOCALE) installed via $_pkg_manager"
|
|
}
|
|
|
|
channel_to_flatpak_branch() {
|
|
case "$CHANNEL" in
|
|
release) echo "" ;;
|
|
beta) echo "beta" ;;
|
|
devedition) error "Firefox Developer Edition is not available as a flatpak" ;;
|
|
nightly) error "Firefox Nightly is not available as a flatpak" ;;
|
|
*) error "Unsupported channel: $CHANNEL" ;;
|
|
esac
|
|
}
|
|
|
|
channel_to_snap_channel() {
|
|
case "$CHANNEL" in
|
|
release) echo "stable" ;;
|
|
beta) echo "beta" ;;
|
|
nightly) echo "edge" ;;
|
|
devedition) error "Firefox Developer Edition is not available as a snap" ;;
|
|
*) error "Unsupported channel: $CHANNEL" ;;
|
|
esac
|
|
}
|
|
|
|
# flathub is always added: runtimes (e.g. org.freedesktop.Platform) live only
|
|
# on flathub, not flathub-beta.
|
|
flatpak_add_remotes() {
|
|
local _branch="$1"
|
|
retry run_sudo flatpak remote-add --if-not-exists --system flathub \
|
|
"https://flathub.org/repo/flathub.flatpakrepo"
|
|
if [ -n "$_branch" ]; then
|
|
retry run_sudo flatpak remote-add --if-not-exists --system flathub-beta \
|
|
"https://flathub.org/beta-repo/flathub-beta.flatpakrepo"
|
|
fi
|
|
}
|
|
|
|
flatpak_install_or_update() {
|
|
local _branch="$1"
|
|
local _remote _ref
|
|
_remote="${_branch:+flathub-beta}"
|
|
_remote="${_remote:-flathub}"
|
|
_ref="org.mozilla.firefox${_branch:+//$_branch}"
|
|
if flatpak info --system "$_ref" >/dev/null 2>&1; then
|
|
retry run_sudo flatpak update --system -y "$_ref"
|
|
else
|
|
retry run_sudo flatpak install --system -y "$_remote" "$_ref"
|
|
fi
|
|
}
|
|
|
|
install_flatpak() {
|
|
info "Installing Firefox ($CHANNEL) via flatpak..."
|
|
need_cmd flatpak
|
|
local _branch
|
|
_branch="$(channel_to_flatpak_branch)"
|
|
flatpak_add_remotes "$_branch"
|
|
flatpak_install_or_update "$_branch"
|
|
ok "Firefox ($CHANNEL, $ARCH, $DETECTED_LOCALE) installed via flatpak"
|
|
}
|
|
|
|
install_snap() {
|
|
info "Installing Firefox ($CHANNEL) via snap..."
|
|
need_cmd snap
|
|
local _channel
|
|
_channel="$(channel_to_snap_channel)"
|
|
if snap list firefox >/dev/null 2>&1; then
|
|
retry run_sudo snap refresh firefox --channel="$_channel"
|
|
else
|
|
retry run_sudo snap install firefox --channel="$_channel"
|
|
fi
|
|
ok "Firefox ($CHANNEL, $ARCH, $DETECTED_LOCALE) installed via snap"
|
|
}
|
|
|
|
tarball_product_name() {
|
|
case "$CHANNEL" in
|
|
release) echo "firefox-latest-ssl" ;;
|
|
beta) echo "firefox-beta-latest-ssl" ;;
|
|
devedition) echo "firefox-devedition-latest-ssl" ;;
|
|
nightly) echo "firefox-nightly-latest-ssl" ;;
|
|
*) error "Unsupported channel: $CHANNEL" ;;
|
|
esac
|
|
}
|
|
|
|
tarball_build_url() {
|
|
local _product _os
|
|
_product="$(tarball_product_name)"
|
|
case "$ARCH" in
|
|
x86_64) _os="linux64" ;;
|
|
aarch64) _os="linux64-aarch64" ;;
|
|
*) error "Unsupported architecture for tarball: $ARCH" ;;
|
|
esac
|
|
echo "https://download.mozilla.org/?product=${_product}&os=${_os}&lang=${DETECTED_LOCALE}"
|
|
}
|
|
|
|
tarball_resolve_final_url() {
|
|
local _url="$1"
|
|
if check_cmd curl; then
|
|
curl --proto '=https' --tlsv1.2 -sSfIL -o /dev/null -w '%{url_effective}' "$_url"
|
|
elif check_cmd wget; then
|
|
wget --https-only --server-response --spider "$_url" 2>&1 \
|
|
| awk '/^ Location: /{url=$2} END{print url}'
|
|
fi
|
|
# No else: if neither downloader is available, returns empty string.
|
|
# Caller uses ${_final_url:-$_url} and skips GPG verification with a warning.
|
|
}
|
|
|
|
tarball_key_url() {
|
|
local _final_url="$1"
|
|
case "$_final_url" in
|
|
*/pub/firefox/releases/*)
|
|
echo "$_final_url" | sed 's|\(/pub/firefox/releases/[^/]*\)/.*|\1/KEY|'
|
|
;;
|
|
*/pub/firefox/nightly/*)
|
|
echo "${_final_url%/*}/KEY"
|
|
;;
|
|
esac
|
|
# Unknown path: return empty; caller will skip GPG verification with a warning.
|
|
}
|
|
|
|
tarball_verify_gpg() {
|
|
local _tarball="$1"
|
|
local _sig_url="$2"
|
|
local _key_url="$3"
|
|
local _gpgdir="$4" # Created and cleaned up by install_tarball's EXIT trap
|
|
local _expected="14F26682D0916CDD81E37B6D61B7B526D98F0353"
|
|
local _actual
|
|
chmod 700 "$_gpgdir"
|
|
download_to_stdout "$_key_url" \
|
|
| gpg --homedir "$_gpgdir" --import >/dev/null 2>&1
|
|
_actual="$(gpg --homedir "$_gpgdir" --with-colons --list-keys 2>/dev/null \
|
|
| awk -F: '/^fpr:/{print $10; exit}')"
|
|
if [ "$_actual" != "$_expected" ]; then
|
|
error "GPG key fingerprint mismatch for tarball: expected $_expected, got ${_actual:-<none>}"
|
|
fi
|
|
download "$_sig_url" "$_gpgdir/firefox.tar.asc"
|
|
if ! gpg --homedir "$_gpgdir" --verify "$_gpgdir/firefox.tar.asc" "$_tarball" >/dev/null 2>&1; then
|
|
error "GPG signature verification of tarball failed"
|
|
fi
|
|
info "Tarball GPG signature verified"
|
|
}
|
|
|
|
tarball_download_and_extract() {
|
|
local _url="$1"
|
|
local _tmpdir="$2"
|
|
local _final_url
|
|
_final_url="$(tarball_resolve_final_url "$_url")"
|
|
download "${_final_url:-$_url}" "$_tmpdir/firefox.tar"
|
|
if [ -n "$_final_url" ] && check_cmd gpg; then
|
|
local _key_url
|
|
_key_url="$(tarball_key_url "$_final_url")"
|
|
if [ -n "$_key_url" ]; then
|
|
local _gpgdir="$_tmpdir/gpg"
|
|
mkdir -p "$_gpgdir"
|
|
tarball_verify_gpg "$_tmpdir/firefox.tar" "${_final_url}.asc" "$_key_url" "$_gpgdir"
|
|
else
|
|
warn "Skipping GPG verification (unrecognized download URL path: $_final_url)"
|
|
fi
|
|
else
|
|
warn "Skipping GPG verification ($([ -z "$_final_url" ] && echo 'could not resolve download URL' || echo 'gpg not found'))"
|
|
fi
|
|
tar -xf "$_tmpdir/firefox.tar" -C "$_tmpdir"
|
|
rm -f "$_tmpdir/firefox.tar"
|
|
}
|
|
|
|
tarball_install_to() {
|
|
local _tmpdir="$1"
|
|
local _install_dir="$2"
|
|
local _pkg_bin="$3"
|
|
local _extracted_dir
|
|
_extracted_dir="$(find "$_tmpdir" -maxdepth 1 -mindepth 1 -type d | head -1)"
|
|
if [ -z "$_extracted_dir" ]; then
|
|
error "Tarball extraction produced no directory in $_tmpdir"
|
|
fi
|
|
# Non-atomic: backup old dir, move new dir in, then remove backup.
|
|
# A crash mid-upgrade leaves no Firefox; acceptable for an installer script.
|
|
run_sudo mv "$_install_dir" "${_install_dir}.old" 2>/dev/null || true
|
|
run_sudo mv "$_extracted_dir" "$_install_dir"
|
|
run_sudo rm -rf "${_install_dir}.old"
|
|
run_sudo ln -sf "$_install_dir/firefox" "/usr/local/bin/${_pkg_bin}"
|
|
}
|
|
|
|
install_tarball() {
|
|
info "Installing Firefox ($CHANNEL) via tarball..."
|
|
need_cmd tar
|
|
local _tmpdir _pkg_bin _install_dir
|
|
_tmpdir="$(mktemp -d)"
|
|
# shellcheck disable=SC2064
|
|
trap "rm -rf '$_tmpdir'" EXIT
|
|
_pkg_bin="$(channel_to_package)"
|
|
_install_dir="/opt/${_pkg_bin}"
|
|
info "Downloading Firefox ($CHANNEL)..."
|
|
tarball_download_and_extract "$(tarball_build_url)" "$_tmpdir"
|
|
tarball_install_to "$_tmpdir" "$_install_dir" "$_pkg_bin"
|
|
rm -rf "$_tmpdir"
|
|
trap - EXIT
|
|
ok "Firefox ($CHANNEL, $ARCH, $DETECTED_LOCALE) installed via tarball"
|
|
}
|
|
|
|
detect_best_method() {
|
|
if [ -n "$INSTALL_METHOD" ]; then
|
|
info "Using forced install method: $INSTALL_METHOD"
|
|
return
|
|
fi
|
|
|
|
if check_cmd apt-get; then
|
|
INSTALL_METHOD="apt"
|
|
elif check_cmd dnf || check_cmd zypper; then
|
|
INSTALL_METHOD="rpm"
|
|
elif check_cmd snap; then
|
|
# snap preferred over flatpak: better system integration on Ubuntu/Debian.
|
|
INSTALL_METHOD="snap"
|
|
elif check_cmd flatpak; then
|
|
INSTALL_METHOD="flatpak"
|
|
else
|
|
INSTALL_METHOD="tarball"
|
|
fi
|
|
|
|
info "Selected install method: $INSTALL_METHOD"
|
|
}
|
|
|
|
install_firefox() {
|
|
if [ "$VERBOSE" = true ]; then set -x; fi
|
|
case "$INSTALL_METHOD" in
|
|
apt) install_apt ;;
|
|
rpm) install_rpm ;;
|
|
flatpak) install_flatpak ;;
|
|
snap) install_snap ;;
|
|
tarball) install_tarball ;;
|
|
*) error "Unknown install method: $INSTALL_METHOD" ;;
|
|
esac
|
|
if [ "$VERBOSE" = true ]; then set +x; fi
|
|
}
|
|
|
|
verify_install() {
|
|
info "Verifying installation..."
|
|
if [ "$INSTALL_METHOD" = "flatpak" ]; then
|
|
local _branch _ref
|
|
_branch="$(channel_to_flatpak_branch)"
|
|
_ref="org.mozilla.firefox${_branch:+//$_branch}"
|
|
if flatpak info --system "$_ref" >/dev/null 2>&1; then
|
|
ok "Verified: Firefox flatpak ($CHANNEL) is installed"
|
|
else
|
|
warn "Could not verify Firefox flatpak installation"
|
|
fi
|
|
return
|
|
fi
|
|
local _pkg_bin _bin _seen _candidate
|
|
_pkg_bin="$(channel_to_package)"
|
|
_seen=""
|
|
for _candidate in "firefox" "$_pkg_bin"; do
|
|
[ "$_candidate" = "$_seen" ] && continue
|
|
_seen="$_candidate"
|
|
_bin="$(command -v "$_candidate" 2>/dev/null || true)"
|
|
if [ -n "$_bin" ]; then
|
|
local _version
|
|
_version="$("$_bin" --version 2>/dev/null || echo "unknown version")"
|
|
ok "$_version (at $_bin)"
|
|
return
|
|
fi
|
|
done
|
|
warn "Could not verify Firefox installation"
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
info "Firefox installer - channel: $CHANNEL"
|
|
|
|
# Verify we have a downloader before doing anything
|
|
if ! check_cmd curl && ! check_cmd wget; then
|
|
error "Either curl or wget is required. Install one and retry."
|
|
fi
|
|
need_cmd uname
|
|
need_cmd mktemp
|
|
|
|
detect_os
|
|
detect_locale
|
|
detect_best_method
|
|
install_firefox
|
|
verify_install
|
|
}
|
|
|
|
if [ "${INSTALL_FIREFOX_SOURCED:-}" != "1" ]; then
|
|
main "$@"
|
|
fi
|