1//===--- Distro.cpp - Linux distribution detection support ------*- C++ -*-===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
9#include "clang/Driver/Distro.h"
10#include "clang/Basic/LLVM.h"
11#include "llvm/ADT/StringRef.h"
12#include "llvm/ADT/StringSwitch.h"
13#include "llvm/Support/ErrorOr.h"
14#include "llvm/Support/MemoryBuffer.h"
15#include "llvm/Support/Threading.h"
16#include "llvm/TargetParser/Host.h"
17#include "llvm/TargetParser/Triple.h"
18
19using namespace clang::driver;
20using namespace clang;
21
22static Distro::DistroType DetectOsRelease(llvm::vfs::FileSystem &VFS) {
23 llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> File =
24 VFS.getBufferForFile(Name: "/etc/os-release");
25 if (!File)
26 File = VFS.getBufferForFile(Name: "/usr/lib/os-release");
27 if (!File)
28 return Distro::UnknownDistro;
29
30 SmallVector<StringRef, 16> Lines;
31 File.get()->getBuffer().split(A&: Lines, Separator: "\n");
32 Distro::DistroType Version = Distro::UnknownDistro;
33
34 // Obviously this can be improved a lot.
35 for (StringRef Line : Lines)
36 if (Version == Distro::UnknownDistro && Line.starts_with(Prefix: "ID="))
37 Version = llvm::StringSwitch<Distro::DistroType>(Line.substr(Start: 3))
38 .Case(S: "alpine", Value: Distro::AlpineLinux)
39 .Case(S: "fedora", Value: Distro::Fedora)
40 .Case(S: "gentoo", Value: Distro::Gentoo)
41 .Case(S: "arch", Value: Distro::ArchLinux)
42 // On SLES, /etc/os-release was introduced in SLES 11.
43 .Case(S: "sles", Value: Distro::OpenSUSE)
44 .Case(S: "opensuse", Value: Distro::OpenSUSE)
45 .Case(S: "exherbo", Value: Distro::Exherbo)
46 .Default(Value: Distro::UnknownDistro);
47 return Version;
48}
49
50static Distro::DistroType DetectLsbRelease(llvm::vfs::FileSystem &VFS) {
51 llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> File =
52 VFS.getBufferForFile(Name: "/etc/lsb-release");
53 if (!File)
54 return Distro::UnknownDistro;
55
56 SmallVector<StringRef, 16> Lines;
57 File.get()->getBuffer().split(A&: Lines, Separator: "\n");
58 Distro::DistroType Version = Distro::UnknownDistro;
59
60 for (StringRef Line : Lines)
61 if (Version == Distro::UnknownDistro &&
62 Line.starts_with(Prefix: "DISTRIB_CODENAME="))
63 Version = llvm::StringSwitch<Distro::DistroType>(Line.substr(Start: 17))
64 .Case(S: "quantal", Value: Distro::UbuntuQuantal)
65 .Case(S: "raring", Value: Distro::UbuntuRaring)
66 .Case(S: "saucy", Value: Distro::UbuntuSaucy)
67 .Case(S: "trusty", Value: Distro::UbuntuTrusty)
68 .Case(S: "utopic", Value: Distro::UbuntuUtopic)
69 .Case(S: "vivid", Value: Distro::UbuntuVivid)
70 .Case(S: "wily", Value: Distro::UbuntuWily)
71 .Case(S: "xenial", Value: Distro::UbuntuXenial)
72 .Case(S: "yakkety", Value: Distro::UbuntuYakkety)
73 .Case(S: "zesty", Value: Distro::UbuntuZesty)
74 .Case(S: "artful", Value: Distro::UbuntuArtful)
75 .Case(S: "bionic", Value: Distro::UbuntuBionic)
76 .Case(S: "cosmic", Value: Distro::UbuntuCosmic)
77 .Case(S: "disco", Value: Distro::UbuntuDisco)
78 .Case(S: "eoan", Value: Distro::UbuntuEoan)
79 .Case(S: "focal", Value: Distro::UbuntuFocal)
80 .Case(S: "groovy", Value: Distro::UbuntuGroovy)
81 .Case(S: "hirsute", Value: Distro::UbuntuHirsute)
82 .Case(S: "impish", Value: Distro::UbuntuImpish)
83 .Case(S: "jammy", Value: Distro::UbuntuJammy)
84 .Case(S: "kinetic", Value: Distro::UbuntuKinetic)
85 .Case(S: "lunar", Value: Distro::UbuntuLunar)
86 .Case(S: "mantic", Value: Distro::UbuntuMantic)
87 .Case(S: "noble", Value: Distro::UbuntuNoble)
88 .Case(S: "oracular", Value: Distro::UbuntuOracular)
89 .Case(S: "plucky", Value: Distro::UbuntuPlucky)
90 .Case(S: "questing", Value: Distro::UbuntuQuesting)
91 .Case(S: "resolute", Value: Distro::UbuntuResolute)
92 .Case(S: "stonking", Value: Distro::UbuntuStonking)
93 .Default(Value: Distro::UnknownDistro);
94 return Version;
95}
96
97static Distro::DistroType DetectDistro(llvm::vfs::FileSystem &VFS) {
98 Distro::DistroType Version = Distro::UnknownDistro;
99
100 // Newer freedesktop.org's compilant systemd-based systems
101 // should provide /etc/os-release or /usr/lib/os-release.
102 Version = DetectOsRelease(VFS);
103 if (Version != Distro::UnknownDistro)
104 return Version;
105
106 // Older systems might provide /etc/lsb-release.
107 Version = DetectLsbRelease(VFS);
108 if (Version != Distro::UnknownDistro)
109 return Version;
110
111 // Otherwise try some distro-specific quirks for Red Hat...
112 llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> File =
113 VFS.getBufferForFile(Name: "/etc/redhat-release");
114
115 if (File) {
116 StringRef Data = File.get()->getBuffer();
117 if (Data.starts_with(Prefix: "Fedora release"))
118 return Distro::Fedora;
119 if (Data.starts_with(Prefix: "Red Hat Enterprise Linux") ||
120 Data.starts_with(Prefix: "CentOS") || Data.starts_with(Prefix: "AlmaLinux") ||
121 Data.starts_with(Prefix: "Rocky Linux") ||
122 Data.starts_with(Prefix: "Scientific Linux")) {
123 if (Data.contains(Other: "release 10"))
124 return Distro::RHEL10;
125 if (Data.contains(Other: "release 9"))
126 return Distro::RHEL9;
127 if (Data.contains(Other: "release 8"))
128 return Distro::RHEL8;
129 if (Data.contains(Other: "release 7"))
130 return Distro::RHEL7;
131 }
132 return Distro::UnknownDistro;
133 }
134
135 // ...for Debian
136 File = VFS.getBufferForFile(Name: "/etc/debian_version");
137 if (File) {
138 StringRef Data = File.get()->getBuffer();
139 // Contents: < major.minor > or < codename/sid >
140 int MajorVersion;
141 if (!Data.split(Separator: '.').first.getAsInteger(Radix: 10, Result&: MajorVersion)) {
142 switch (MajorVersion) {
143 case 8:
144 return Distro::DebianJessie;
145 case 9:
146 return Distro::DebianStretch;
147 case 10:
148 return Distro::DebianBuster;
149 case 11:
150 return Distro::DebianBullseye;
151 case 12:
152 return Distro::DebianBookworm;
153 case 13:
154 return Distro::DebianTrixie;
155 case 14:
156 return Distro::DebianForky;
157 case 15:
158 return Distro::DebianDuke;
159 default:
160 return Distro::UnknownDistro;
161 }
162 }
163 return llvm::StringSwitch<Distro::DistroType>(Data.split(Separator: "\n").first)
164 .Case(S: "jessie/sid", Value: Distro::DebianJessie)
165 .Case(S: "stretch/sid", Value: Distro::DebianStretch)
166 .Case(S: "buster/sid", Value: Distro::DebianBuster)
167 .Case(S: "bullseye/sid", Value: Distro::DebianBullseye)
168 .Case(S: "bookworm/sid", Value: Distro::DebianBookworm)
169 .Case(S: "trixie/sid", Value: Distro::DebianTrixie)
170 .Case(S: "forky/sid", Value: Distro::DebianForky)
171 .Case(S: "duke/sid", Value: Distro::DebianDuke)
172 .Default(Value: Distro::UnknownDistro);
173 }
174
175 // ...for SUSE
176 File = VFS.getBufferForFile(Name: "/etc/SuSE-release");
177 if (File) {
178 StringRef Data = File.get()->getBuffer();
179 SmallVector<StringRef, 8> Lines;
180 Data.split(A&: Lines, Separator: "\n");
181 for (const StringRef &Line : Lines) {
182 if (!Line.trim().starts_with(Prefix: "VERSION"))
183 continue;
184 std::pair<StringRef, StringRef> SplitLine = Line.split(Separator: '=');
185 // Old versions have split VERSION and PATCHLEVEL
186 // Newer versions use VERSION = x.y
187 std::pair<StringRef, StringRef> SplitVer =
188 SplitLine.second.trim().split(Separator: '.');
189 int Version;
190
191 // OpenSUSE/SLES 10 and older are not supported and not compatible
192 // with our rules, so just treat them as Distro::UnknownDistro.
193 if (!SplitVer.first.getAsInteger(Radix: 10, Result&: Version) && Version > 10)
194 return Distro::OpenSUSE;
195 return Distro::UnknownDistro;
196 }
197 return Distro::UnknownDistro;
198 }
199
200 // ...and others.
201 if (VFS.exists(Path: "/etc/gentoo-release"))
202 return Distro::Gentoo;
203
204 return Distro::UnknownDistro;
205}
206
207static Distro::DistroType GetDistro(llvm::vfs::FileSystem &VFS,
208 const llvm::Triple &TargetOrHost) {
209 // If we don't target Linux, no need to check the distro. This saves a few
210 // OS calls.
211 if (!TargetOrHost.isOSLinux())
212 return Distro::UnknownDistro;
213
214 // True if we're backed by a real file system.
215 const bool onRealFS = (llvm::vfs::getRealFileSystem() == &VFS);
216
217 // If the host is not running Linux, and we're backed by a real file
218 // system, no need to check the distro. This is the case where someone
219 // is cross-compiling from BSD or Windows to Linux, and it would be
220 // meaningless to try to figure out the "distro" of the non-Linux host.
221 llvm::Triple HostTriple(llvm::sys::getProcessTriple());
222 if (!HostTriple.isOSLinux() && onRealFS)
223 return Distro::UnknownDistro;
224
225 if (onRealFS) {
226 // If we're backed by a real file system, perform
227 // the detection only once and save the result.
228 static const Distro::DistroType LinuxDistro = DetectDistro(VFS);
229 return LinuxDistro;
230 }
231 // This is mostly for passing tests which uses llvm::vfs::InMemoryFileSystem,
232 // which is not "real".
233 return DetectDistro(VFS);
234}
235
236Distro::Distro(llvm::vfs::FileSystem &VFS, const llvm::Triple &TargetOrHost)
237 : DistroVal(GetDistro(VFS, TargetOrHost)) {}
238