master ohmu/fastfetch-openbsd / src / detection / os / os_linux.c
ohmu  ·  2026-10-15
  1#include "os.h"
  2#include "common/properties.h"
  3#include "common/parsing.h"
  4#include "common/io.h"
  5#include "common/processing.h"
  6#include "common/strutil.h"
  7
  8#include <string.h>
  9#include <stdlib.h>
 10
 11static bool parseLsbRelease(const char* fileName, FFOSResult* result) {
 12    return ffParsePropFileValues(fileName, 4, (FFpropquery[]) {
 13                                                  { "DISTRIB_ID =", &result->id },
 14                                                  { "DISTRIB_DESCRIPTION =", &result->prettyName },
 15                                                  { "DISTRIB_RELEASE =", &result->version },
 16                                                  { "DISTRIB_CODENAME =", &result->codename },
 17                                              });
 18}
 19
 20static bool parseOsRelease(const char* fileName, FFOSResult* result) {
 21    return ffParsePropFileValues(fileName, 11, (FFpropquery[]) {
 22                                                   { "PRETTY_NAME =", &result->prettyName },
 23                                                   { "NAME =", &result->name },
 24                                                   { "ID =", &result->id },
 25                                                   { "ID_LIKE =", &result->idLike },
 26                                                   { "VARIANT =", &result->variant },
 27                                                   { "VARIANT_ID =", &result->variantID },
 28                                                   { "VERSION =", &result->version },
 29                                                   { "VERSION_ID =", &result->versionID },
 30                                                   { "VERSION_CODENAME =", &result->codename },
 31                                                   { "CODENAME =", &result->codename },
 32                                                   { "BUILD_ID =", &result->buildID },
 33                                               });
 34}
 35
 36// Common logic for detecting Armbian image version
 37[[maybe_unused]] static bool detectArmbianVersion(FFOSResult* result) {
 38    // Possible values `PRETTY_NAME` starts with on Armbian:
 39    // - `Armbian` for official releases
 40    // - `Armbian_community` for community releases
 41    // - `Armbian_Security` for images with kali repo added
 42    // - `Armbian-unofficial` for an unofficial image built from source, e.g. during development and testing
 43    if (ffStrbufStartsWithS(&result->prettyName, "Armbian")) {
 44        ffStrbufSetStatic(&result->name, "Armbian");
 45    } else {
 46        return false;
 47    }
 48    ffStrbufSet(&result->idLike, &result->id);
 49    ffStrbufSetS(&result->id, "armbian");
 50    ffStrbufClear(&result->versionID);
 51    uint32_t versionStart = ffStrbufFirstIndexC(&result->prettyName, ' ') + 1;
 52    uint32_t versionEnd = ffStrbufNextIndexC(&result->prettyName, versionStart, ' ');
 53    ffStrbufSetNS(&result->versionID, versionEnd - versionStart, result->prettyName.chars + versionStart);
 54    return true;
 55}
 56
 57// Returns false if PrettyName should be updated by caller
 58[[maybe_unused]] static bool getUbuntuFlavour(FFOSResult* result) {
 59    if (detectArmbianVersion(result)) {
 60        return true;
 61    } else if (ffStrbufStartsWithS(&result->prettyName, "Linux Lite ")) {
 62        ffStrbufSetStatic(&result->name, "Linux Lite");
 63        ffStrbufSetStatic(&result->id, "linuxlite");
 64        ffStrbufSetStatic(&result->idLike, "ubuntu");
 65        ffStrbufSetS(&result->versionID, result->prettyName.chars + strlen("Linux Lite "));
 66        return true;
 67    } else if (ffStrbufStartsWithS(&result->prettyName, "Rhino Linux ")) {
 68        ffStrbufSetStatic(&result->name, "Rhino Linux");
 69        ffStrbufSetStatic(&result->id, "rhinolinux");
 70        ffStrbufSetStatic(&result->idLike, "ubuntu");
 71        ffStrbufSetS(&result->versionID, result->prettyName.chars + strlen("Rhino Linux "));
 72        return true;
 73    } else if (ffStrbufStartsWithS(&result->prettyName, "VanillaOS ")) {
 74        ffStrbufSetStatic(&result->id, "vanilla");
 75        ffStrbufSetStatic(&result->idLike, "ubuntu");
 76        return true;
 77    }
 78
 79    if (ffPathExists("/usr/bin/lliurex-version", FF_PATHTYPE_FILE)) {
 80        ffStrbufSetStatic(&result->name, "LliureX");
 81        ffStrbufSetStatic(&result->id, "lliurex");
 82        ffStrbufClear(&result->version);
 83        if (ffProcessAppendStdOut(&result->version, (char* const[]) {
 84                                                        "/usr/bin/lliurex-version",
 85                                                        nullptr,
 86                                                    }) == nullptr) { // 8.2.2
 87            ffStrbufTrimRightSpace(&result->version);
 88        }
 89        ffStrbufSetF(&result->prettyName, "LliureX %s", result->version.chars);
 90        ffStrbufSetStatic(&result->idLike, "ubuntu");
 91        return true;
 92    }
 93
 94    // xdgConfigDirs contains plasma only
 95    // `ubuntustudio-desktop-core` is installed on both the full and core installations
 96    if (ffPathExists("/var/lib/dpkg/info/ubuntustudio-desktop-core.list", FF_PATHTYPE_FILE)) {
 97        ffStrbufSetStatic(&result->name, "Ubuntu Studio");
 98        ffStrbufSetStatic(&result->id, "ubuntu-studio");
 99        ffStrbufSetStatic(&result->idLike, "ubuntu");
100        return false;
101    }
102
103    const char* xdgConfigDirs = getenv("XDG_CONFIG_DIRS");
104    if (!ffStrSet(xdgConfigDirs)) {
105        return false;
106    }
107
108    if (ffStrContains(xdgConfigDirs, "kde") || ffStrContains(xdgConfigDirs, "plasma") || ffStrContains(xdgConfigDirs, "kubuntu")) {
109        ffStrbufSetStatic(&result->name, "Kubuntu");
110        ffStrbufSetStatic(&result->id, "kubuntu");
111        ffStrbufSetStatic(&result->idLike, "ubuntu");
112        return false;
113    }
114
115    if (ffStrContains(xdgConfigDirs, "xfce") || ffStrContains(xdgConfigDirs, "xubuntu")) {
116        ffStrbufSetStatic(&result->name, "Xubuntu");
117        ffStrbufSetStatic(&result->id, "xubuntu");
118        ffStrbufSetStatic(&result->idLike, "ubuntu");
119        return false;
120    }
121
122    if (ffStrContains(xdgConfigDirs, "lxqt") || ffStrContains(xdgConfigDirs, "lubuntu")) {
123        ffStrbufSetStatic(&result->name, "Lubuntu");
124        ffStrbufSetStatic(&result->id, "lubuntu");
125        ffStrbufSetStatic(&result->idLike, "ubuntu");
126        return false;
127    }
128
129    if (ffStrContains(xdgConfigDirs, "budgie")) {
130        ffStrbufSetStatic(&result->name, "Ubuntu Budgie");
131        ffStrbufSetStatic(&result->id, "ubuntu-budgie");
132        ffStrbufSetStatic(&result->idLike, "ubuntu");
133        return false;
134    }
135
136    if (ffStrContains(xdgConfigDirs, "cinnamon")) {
137        ffStrbufSetStatic(&result->name, "Ubuntu Cinnamon");
138        ffStrbufSetStatic(&result->id, "ubuntu-cinnamon");
139        ffStrbufSetStatic(&result->idLike, "ubuntu");
140        return false;
141    }
142
143    if (ffStrContains(xdgConfigDirs, "mate")) {
144        ffStrbufSetStatic(&result->name, "Ubuntu MATE");
145        ffStrbufSetStatic(&result->id, "ubuntu-mate");
146        ffStrbufSetStatic(&result->idLike, "ubuntu");
147        return false;
148    }
149
150    if (ffStrContains(xdgConfigDirs, "ukui")) {
151        ffStrbufSetStatic(&result->name, "Ubuntu Kylin");
152        ffStrbufSetStatic(&result->id, "ubuntu-kylin");
153        ffStrbufSetStatic(&result->idLike, "ubuntu");
154        return false;
155    }
156
157    if (ffStrContains(xdgConfigDirs, "sway")) {
158        ffStrbufSetStatic(&result->name, "Ubuntu Sway");
159        ffStrbufSetStatic(&result->id, "ubuntu-sway");
160        ffStrbufSetStatic(&result->idLike, "ubuntu");
161        return false;
162    }
163
164    if (ffStrContains(xdgConfigDirs, "touch")) {
165        ffStrbufSetStatic(&result->name, "Ubuntu Touch");
166        ffStrbufSetStatic(&result->id, "ubuntu-touch");
167        ffStrbufSetStatic(&result->idLike, "ubuntu");
168        return false;
169    }
170
171    if (ffStrContains(xdgConfigDirs, "unity")) {
172        ffStrbufSetStatic(&result->name, "Ubuntu Unity");
173        ffStrbufSetStatic(&result->id, "ubuntu-unity");
174        ffStrbufSetStatic(&result->idLike, "ubuntu");
175        return false;
176    }
177
178    return false;
179}
180
181[[maybe_unused]] static void getDebianVersion(FFOSResult* result) {
182    FF_STRBUF_AUTO_DESTROY debianVersion = ffStrbufCreate();
183    ffAppendFileBuffer("/etc/debian_version", &debianVersion);
184    ffStrbufTrimRightSpace(&debianVersion);
185    if (!debianVersion.length) {
186        return;
187    }
188    ffStrbufDestroy(&result->versionID);
189    ffStrbufInitMove(&result->versionID, &debianVersion);
190
191    ffStrbufSetF(&result->prettyName, "%s %s (%s)", result->name.chars, result->versionID.chars, result->codename.chars);
192}
193
194[[maybe_unused]] static bool detectDebianDerived(FFOSResult* result) {
195    if (detectArmbianVersion(result)) {
196        return true;
197    } else if (ffStrbufStartsWithS(&result->name, "Loc-OS")) {
198        ffStrbufSetStatic(&result->id, "locos");
199        ffStrbufSetStatic(&result->idLike, "debian");
200        return true;
201    } else if (ffStrbufEqualS(&result->name, "Parrot Security")) {
202        // https://github.com/ParrotSec/base-files/blob/c06f6d42ddf8d79564882306576576eddab7d907/etc/os-release
203        ffStrbufSetS(&result->id, "parrot");
204        ffStrbufSetS(&result->idLike, "debian");
205        return true;
206    } else if (ffStrbufStartsWithS(&result->name, "Lilidog GNU/Linux")) {
207        // https://github.com/fastfetch-cli/fastfetch/issues/1373
208        ffStrbufSetStatic(&result->id, "lilidog");
209        ffStrbufSetStatic(&result->idLike, "debian");
210        return true;
211    } else if (access("/usr/bin/pveversion", X_OK) == 0) {
212        ffStrbufSetStatic(&result->id, "proxmox");
213        ffStrbufSetStatic(&result->idLike, "debian");
214        ffStrbufSetStatic(&result->name, "Proxmox VE");
215        ffStrbufClear(&result->versionID);
216        if (ffProcessAppendStdOut(&result->versionID, (char* const[]) {
217                                                          "/usr/bin/dpkg-query",
218                                                          "--showformat=${version}",
219                                                          "--show",
220                                                          "pve-manager",
221                                                          nullptr,
222                                                      }) == nullptr) { // 8.2.2
223            ffStrbufTrimRightSpace(&result->versionID);
224            ffStrbufSetStatic(&result->prettyName, "Proxmox VE ");
225            ffStrbufAppend(&result->prettyName, &result->versionID);
226        } else {
227            ffStrbufSetStatic(&result->prettyName, "Proxmox VE");
228        }
229        return true;
230    } else if (access("/usr/sbin/proxmox-backup-manager", X_OK) == 0) {
231        ffStrbufSetStatic(&result->id, "proxmox");
232        ffStrbufSetStatic(&result->idLike, "debian");
233        ffStrbufSetStatic(&result->name, "Proxmox Backup Server");
234        ffStrbufClear(&result->versionID);
235        if (ffProcessAppendStdOut(&result->versionID, (char* const[]) {
236                                                          "/usr/bin/dpkg-query",
237                                                          "--showformat=${version}",
238                                                          "--show",
239                                                          "proxmox-backup-server",
240                                                          nullptr,
241                                                      }) == nullptr) {
242            ffStrbufTrimRightSpace(&result->versionID);
243            ffStrbufSetStatic(&result->prettyName, "Proxmox Backup Server ");
244            ffStrbufAppend(&result->prettyName, &result->versionID);
245        } else {
246            ffStrbufSetStatic(&result->prettyName, "Proxmox Backup Server");
247        }
248        return true;
249    } else if (ffPathExists("/etc/rpi-issue", FF_PATHTYPE_FILE)) {
250        // Raspberry Pi OS
251        ffStrbufSetStatic(&result->id, "raspbian");
252        ffStrbufSetStatic(&result->idLike, "debian");
253        ffStrbufSetStatic(&result->name, "Raspberry Pi OS");
254        getDebianVersion(result);
255        return true;
256    } else if (ffPathExists("/boot/dietpi/.version", FF_PATHTYPE_FILE)) {
257        // DietPi
258        ffStrbufSetStatic(&result->id, "dietpi");
259        ffStrbufSetStatic(&result->name, "DietPi");
260        ffStrbufSetStatic(&result->prettyName, "DietPi");
261        ffStrbufSetStatic(&result->idLike, "debian");
262        FF_STRBUF_AUTO_DESTROY core = ffStrbufCreate();
263        FF_STRBUF_AUTO_DESTROY sub = ffStrbufCreate();
264        FF_STRBUF_AUTO_DESTROY rc = ffStrbufCreate();
265        if (ffParsePropFileValues("/boot/dietpi/.version", 3, (FFpropquery[]) {
266                                                                  { "G_DIETPI_VERSION_CORE=", &core },
267                                                                  { "G_DIETPI_VERSION_SUB=", &sub },
268                                                                  { "G_DIETPI_VERSION_RC=", &rc },
269                                                              })) {
270            ffStrbufAppendF(&result->prettyName, " %s.%s.%s", core.chars, sub.chars, rc.chars);
271        }
272        return true;
273    } else if (ffStrbufEndsWithS(&instance.state.platform.sysinfo.release, "+truenas")) {
274        // TrueNAS Scale
275        ffStrbufSetStatic(&result->id, "truenas-scale");
276        ffStrbufSetStatic(&result->idLike, "debian");
277        ffStrbufSetStatic(&result->name, "TrueNAS Scale");
278        ffStrbufSetStatic(&result->prettyName, "TrueNAS Scale");
279        return true;
280    } else if (ffPathExists("/usr/bin/emmabuntus_config.sh", FF_PATHTYPE_FILE)) {
281        // Emmabuntüs
282        ffStrbufSetStatic(&result->id, "emmabuntus");
283        ffStrbufSetStatic(&result->idLike, "debian");
284        ffStrbufSetStatic(&result->name, "Emmabuntüs");
285        getDebianVersion(result);
286        return true;
287    } else {
288        // Hack for MX Linux. See #847
289        FF_STRBUF_AUTO_DESTROY lsbRelease = ffStrbufCreate();
290        if (ffAppendFileBuffer("/etc/lsb-release", &lsbRelease) && ffStrbufContainS(&lsbRelease, "DISTRIB_ID=MX")) {
291            ffStrbufSetStatic(&result->id, "mx");
292            ffStrbufSetStatic(&result->idLike, "debian");
293            ffStrbufSetStatic(&result->name, "MX");
294
295            ffStrbufClear(&result->version);
296            ffParsePropLines(lsbRelease.chars, "DISTRIB_RELEASE=", &result->version);
297            ffStrbufSet(&result->versionID, &result->version);
298
299            ffStrbufClear(&result->codename);
300            ffParsePropLines(lsbRelease.chars, "DISTRIB_CODENAME=", &result->codename);
301
302            ffStrbufClear(&result->prettyName);
303            ffParsePropLines(lsbRelease.chars, "DISTRIB_DESCRIPTION=", &result->prettyName);
304            return true;
305        }
306    }
307    return false;
308}
309
310[[maybe_unused]] static bool detectFedoraVariant(FFOSResult* result) {
311    if (ffStrbufEqualS(&result->variantID, "coreos") || ffStrbufEqualS(&result->variantID, "kinoite") || ffStrbufEqualS(&result->variantID, "sericea") || ffStrbufEqualS(&result->variantID, "silverblue")) {
312        ffStrbufAppendC(&result->id, '-');
313        ffStrbufAppend(&result->id, &result->variantID);
314        ffStrbufSetStatic(&result->idLike, "fedora");
315        return true;
316    }
317    return false;
318}
319
320[[maybe_unused]] static bool detectBedrock(FFOSResult* os) {
321    const char* bedrockRestrict = getenv("BEDROCK_RESTRICT");
322    if (bedrockRestrict && bedrockRestrict[0] == '1') {
323        return false;
324    }
325    return parseOsRelease(FASTFETCH_TARGET_DIR_ROOT "/bedrock/strata/bedrock/etc/os-release", os);
326}
327
328[[maybe_unused]] static void detectDeepinEnhancement(FFOSResult* result) {
329    if (ffStrbufContainC(&result->prettyName, '(')) {
330        return;
331    }
332
333    FF_STRBUF_AUTO_DESTROY minor = ffStrbufCreate();
334    FF_STRBUF_AUTO_DESTROY edition = ffStrbufCreate();
335
336    if (!ffParsePropFileValues(
337            FASTFETCH_TARGET_DIR_ETC "/os-version",
338            2,
339            (FFpropquery[]) {
340                { "MinorVersion=", &minor },
341                { "EditionName=", &edition },
342            }) ||
343        minor.length == 0) {
344        return;
345    }
346
347    ffStrbufSet(&result->versionID, &minor);
348
349    if (edition.length > 0) {
350        ffStrbufSetF(&result->prettyName, "%s %s (%s)", result->name.chars, minor.chars, edition.chars);
351    } else {
352        ffStrbufSetF(&result->prettyName, "%s %s", result->name.chars, minor.chars);
353    }
354}
355
356[[maybe_unused]] static void detectAstraVersion(FFOSResult* result) {
357    // `PRETTY_NAME` is just `Astra Linux`; the version is in `VERSION_ID`, e.g. `2.12_x86-64`
358    if (result->version.length == 0) { // Should be empty. Just in case
359        ffStrbufAppendSUntilC(&result->version, result->versionID.chars, '_');
360    }
361    if (result->version.length > 0) {
362        ffStrbufAppendC(&result->prettyName, ' ');
363        ffStrbufAppend(&result->prettyName, &result->version);
364    }
365}
366
367static void detectOS(FFOSResult* os) {
368#ifdef FF_CUSTOM_OS_RELEASE_PATH
369    parseOsRelease(FF_STR(FF_CUSTOM_OS_RELEASE_PATH), os);
370    #ifdef FF_CUSTOM_LSB_RELEASE_PATH
371    parseLsbRelease(FF_STR(FF_CUSTOM_LSB_RELEASE_PATH), os);
372    #endif
373    return;
374#endif
375
376#ifdef __linux__
377    if (detectBedrock(os)) {
378        return;
379    }
380#endif
381
382    // Refer: https://gist.github.com/natefoo/814c5bf936922dad97ff
383
384    parseOsRelease(FASTFETCH_TARGET_DIR_ETC "/os-release", os);
385
386    if (os->id.length == 0 || os->version.length == 0 || os->prettyName.length == 0 || os->codename.length == 0) {
387        parseLsbRelease(FASTFETCH_TARGET_DIR_ETC "/lsb-release", os);
388    }
389    if (os->id.length == 0 || os->name.length == 0 || os->prettyName.length == 0) {
390        parseOsRelease(FASTFETCH_TARGET_DIR_USR "/lib/os-release", os);
391    }
392    if (os->id.length == 0 && os->name.length == 0 && os->prettyName.length == 0) {
393        // HarmonyOS has no os-release file
394        if (ffStrbufEqualS(&instance.state.platform.sysinfo.name, "HarmonyOS")) {
395            ffStrbufSetS(&os->id, "harmonyos");
396            ffStrbufSetS(&os->idLike, "harmonyos");
397            ffStrbufSetS(&os->name, "HarmonyOS");
398            ffStrbufSetS(&os->prettyName, "HarmonyOS");
399        }
400    }
401}
402
403void ffDetectOSImpl(FFOSResult* os) {
404    detectOS(os);
405
406#if __linux__ || __GNU__
407    if (ffStrbufEqualS(&os->id, "ubuntu")) {
408        if (!getUbuntuFlavour(os)) {
409            if (!ffStrbufEndsWithS(&os->prettyName, " (development branch)")) {
410                ffStrbufSetF(&os->prettyName, "%s %s", os->name.chars, os->version.chars); // os->version contains code name
411            }
412        }
413    } else if (ffStrbufEqualS(&os->id, "debian")) {
414        if (!detectDebianDerived(os)) {
415            getDebianVersion(os);
416        }
417    } else if (ffStrbufEqualS(&os->id, "fedora")) {
418        detectFedoraVariant(os);
419    } else if (ffStrbufEqualS(&os->id, "linuxmint")) {
420        if (ffStrbufEqualS(&os->name, "LMDE")) {
421            ffStrbufSetS(&os->id, "lmde");
422            ffStrbufSetS(&os->idLike, "linuxmint");
423        }
424    } else if (ffStrbufEqualS(&os->id, "deepin")) {
425        detectDeepinEnhancement(os);
426    } else if (ffStrbufEqualS(&os->id, "astra")) {
427        detectAstraVersion(os);
428    }
429#endif
430}