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