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}