src/distro.js
Rama main · réplica de lectura del repositorio original.
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* Distro detection, in one place. Parses /etc/os-release fields (uppercase on
* real systems, lowercase in test stubs) into a normalized profile that every
* check can rely on instead of doing its own string matching.
*/
export function detectDistro(osRelease = {}) {
const get = (k) =>
String(osRelease[k] || osRelease[k.toUpperCase()] || osRelease[k.toLowerCase()] || "").toLowerCase();
const id = get("id");
const idLike = get("id_like");
const variant = get("variant");
const all = `${id} ${idLike} ${variant}`;
// Image-based (ostree/atomic) distros update atomically via rpm-ostree,
// and their root filesystem is virtual. Checked before family detection.
const imageBased = /bazzite|silverblue|kinoite|sericea|bluefin|atomic|ostree/.test(all);
// The specific atomic variant, so messages can name the distro ("Bazzite"
// instead of a generic "immutable system"). null on classic distros.
const variantMatch = all.match(/(bazzite|silverblue|kinoite|sericea|bluefin|aurora|cosmic-atomic|ublue|bootc)/);
const atomicVariant = variantMatch ? variantMatch[1] : (imageBased ? "atomic" : null);
let family = "other";
if (id === "alpine") family = "alpine";
else if (id === "void") family = "void";
else if (id === "gentoo") family = "gentoo";
else if (id === "arch" || /manjaro|endeavouros/.test(id) || /arch/.test(idLike)) family = "arch";
else if (/suse|opensuse/.test(`${id} ${idLike}`)) family = "suse";
else if (id === "debian" || id === "ubuntu" || /debian|ubuntu/.test(idLike)) family = "debian";
else if (/fedora|rhel|centos|rocky|almalinux|nobara|bazzite|silverblue|kinoite/.test(all)) family = "fedora";
let pkg = "unknown";
if (imageBased) pkg = "rpm-ostree";
else if (family === "fedora") pkg = "dnf";
else if (family === "debian") pkg = "apt";
else if (family === "arch") pkg = "pacman";
else if (family === "suse") pkg = "zypper";
else if (family === "alpine") pkg = "apk";
else if (family === "void") pkg = "xbps";
else if (family === "gentoo") pkg = "emerge";
// Init system — the commands a fix may suggest for services. Alpine and
// Gentoo run OpenRC, Void runs runit, everything else here is systemd.
let init = "systemd";
if (family === "alpine" || family === "gentoo") init = "openrc";
else if (family === "void") init = "runit";
return { id, idLike, variant, family, pkg, imageBased, atomicVariant, init, all };
}
/**
* Central distro-specific install helper — single source of truth for fix strings.
* `pkgs` can be:
* - a string ("pipewire") or array (["podman","buildah"]) when the package
* has the same name everywhere, or
* - an object mapping package-manager/family → name for packages that are
* named differently across distros ({ fedora: "procps-ng", "*": "procps" }).
* Keys are matched against dist.pkg first (dnf/apt/pacman/zypper/apk/
* rpm-ostree), then dist.family, then "*" as the fallback.
* Returns a copy-pasteable `sudo ... install` line for the current distro.
* Keeps fix messages from drifting across 20+ checks.
*/
export function pkgInstall(dist, pkgs) {
if (pkgs && typeof pkgs === "object" && !Array.isArray(pkgs)) {
const pick = pkgs[dist?.pkg] ?? pkgs[dist?.family] ?? pkgs["*"];
if (!pick) throw new Error(`pkgInstall: no package name for ${dist?.pkg || dist?.family || "unknown"} and no "*" fallback`);
return pkgInstall(dist, pick);
}
const list = Array.isArray(pkgs) ? pkgs.join(" ") : String(pkgs);
const p = dist?.pkg;
// An unknown manager must never degrade to a wrong command — on NixOS or an
// exotic system, "sudo dnf install" is bad advice, not a fallback.
if (!p || p === "unknown") {
return `Manual step: install ${list} with your distribution's package manager (not recognized by linux-doctor).`;
}
if (p === "apt") return `sudo apt install ${list}`;
if (p === "apk") return `sudo apk add ${list}`;
if (p === "pacman") return `sudo pacman -S ${list}`;
if (p === "zypper") return `sudo zypper install ${list}`;
if (p === "xbps") return `sudo xbps-install -Sy ${list}`;
if (p === "emerge") return `sudo emerge ${list}`;
// Image-based/atomic desktops (Bazzite, Silverblue, Kinoite…): layering is
// the correct command for a genuinely missing system package, but it only
// applies on the next boot.
if (p === "rpm-ostree") return `sudo rpm-ostree install ${list}, then reboot`;
return `sudo dnf install ${list}`;
}
/**
* How to reclaim space held by the package manager's cache. "There is no
* package cache" is a real answer here: an image-based system has no dnf or
* apt cache at all — what exists is rpm-ostree's cached repo metadata and
* transient deployment space.
* Returns a phrase to embed in a fix sentence.
*/
export function pkgCacheClean(dist) {
const p = dist?.pkg;
if (p === "apt") return "`sudo apt clean`";
if (p === "dnf") return "`sudo dnf clean all`";
if (p === "pacman") return "`sudo pacman -Sc`";
if (p === "zypper") return "`sudo zypper clean`";
if (p === "apk") return "`sudo apk cache clean`";
if (p === "xbps") return "`sudo xbps-remove -O`";
if (p === "emerge") return "`sudo eclean-dist` (gentoolkit)";
if (p === "rpm-ostree") {
return "`sudo rpm-ostree cleanup -m` (cached repo metadata — there is no package cache to clear on an image-based system)";
}
return "your distribution's package-cache tool";
}
/**
* How to get rid of old kernels. Returns a phrase, because on some systems the
* honest answer is "there is nothing to remove": Arch ships one kernel version
* at a time, and on an image-based system the kernel belongs to the image.
*/
export function oldKernelClean(dist) {
const p = dist?.pkg;
if (p === "apt") return "`sudo apt autoremove --purge`";
if (p === "dnf") return "`sudo dnf remove --oldinstallonly`";
if (p === "zypper") return "`sudo zypper purge-kernels`";
if (p === "xbps") return "`sudo vkpurge rm all`";
if (p === "emerge") return "`sudo eclean-kernel` (app-admin/eclean-kernel)";
if (p === "pacman") return "nothing — Arch ships one kernel version at a time; old ones only linger in the package cache (`sudo pacman -Sc`)";
if (p === "rpm-ostree") {
return "nothing to remove by hand — the kernel is part of the image, and old kernels leave with their deployments (`sudo rpm-ostree cleanup -b` frees the space)";
}
return "remove old kernels with your distribution's tool";
}
/**
* How to refresh the bootloader configuration. An image-based system is the
* case that matters: boot entries are generated per deployment by rpm-ostree,
* and running grub2-mkconfig there by hand is the classic wrong move.
*/
export function bootloaderRefresh(dist) {
const p = dist?.pkg;
if (p === "rpm-ostree") {
return "do not regenerate GRUB by hand on an image-based system — boot entries are generated per deployment; use `sudo rpm-ostree kargs` for kernel arguments (and `ujust configure-grub` for menu visibility on Bazzite)";
}
if (dist?.family === "debian") return "`sudo update-grub`";
if (dist?.family === "arch") return "`sudo grub-mkconfig -o /boot/grub/grub.cfg` (or `sudo bootctl update` for systemd-boot)";
if (dist?.family === "suse") return "`sudo grub2-mkconfig -o /boot/grub2/grub.cfg` (note the `2` and `/boot/grub2/` on openSUSE)";
if (dist?.family === "fedora") return "`sudo grub2-mkconfig -o /boot/grub2/grub.cfg`";
return "refresh the bootloader with your distribution's tool";
}
/**
* The service-manager command for an action on a unit. Returns a BARE command
* string (callers wrap it in backticks), or null when the concept has no
* equivalent on that init system — telling an OpenRC or runit user to run
* `systemctl` is simply wrong.
* Actions: status · start · stop · restart · reload · enable-now.
*/
export function svcCmd(dist, action, unit) {
const init = dist?.init || "systemd";
// systemd units carry a .service suffix; OpenRC and runit service names
// do not, so `rc-service foo.service` would be wrong.
const u = init === "systemd" ? unit : String(unit).replace(/\.service$/, "");
if (init === "openrc") {
if (action === "status") return `rc-service ${u} status`;
if (action === "start") return `sudo rc-service ${u} start`;
if (action === "stop") return `sudo rc-service ${u} stop`;
if (action === "restart") return `sudo rc-service ${u} restart`;
if (action === "reload") return `sudo rc-service ${u} reload`;
if (action === "enable-now") return `sudo rc-update add ${u} && sudo rc-service ${u} start`;
return null;
}
if (init === "runit") {
if (action === "status") return `sv status ${u}`;
if (action === "start") return `sudo sv start ${u}`;
if (action === "stop") return `sudo sv stop ${u}`;
if (action === "restart") return `sudo sv restart ${u}`;
if (action === "reload") return `sudo sv reload ${u}`;
if (action === "enable-now") return `sudo ln -s /etc/sv/${u} /var/service/`;
return null;
}
if (action === "status") return `systemctl status ${u}`;
if (action === "start") return `sudo systemctl start ${u}`;
if (action === "stop") return `sudo systemctl stop ${u}`;
if (action === "restart") return `sudo systemctl restart ${u}`;
if (action === "reload") return `sudo systemctl reload ${u}`;
if (action === "enable-now") return `sudo systemctl enable --now ${u}`;
return null;
}
/**
* Where to read a unit's log output. systemd has `journalctl -u`; OpenRC and
* runit write to a system log instead, and it is a different file on each.
*/
export function svcLogs(dist, unit) {
const init = dist?.init || "systemd";
if (init === "openrc") return `grep ${unit} /var/log/messages | tail -n 50`;
if (init === "runit") return `tail -n 50 /var/log/${unit}/current`;
return `journalctl -u ${unit} -b`;
}
/**
* Timer units exist on systemd only. A fix that says "enable fstrim.timer" is
* meaningless on OpenRC and runit, where the equivalent is a cron job.
*/
export function hasTimers(dist) {
return (dist?.init || "systemd") === "systemd";
}