CMake: Der Fall, in dem das Projekt unverzeihlich ist, ist die Qualität seines Codes

Bild 1

CMake ist ein plattformübergreifendes Automatisierungssystem für Bauprojekte. Dieses System ist viel älter als der statische Code-Analysator PVS-Studio, während noch niemand versucht hat, es auf den Code anzuwenden und Fehler zu überprüfen. Es stellt sich heraus, dass es viele Fehler gibt. Das Publikum von CMake ist riesig. Darauf beginnen neue Projekte und alte werden übertragen. Es ist beängstigend, sich vorzustellen, wie viele Programmierer diesen oder jenen Fehler haben könnten.

Einführung


CMake (vom englischen plattformübergreifenden Hersteller) ist ein plattformübergreifendes Automatisierungssystem zum Erstellen von Software aus dem Quellcode. CMake erstellt nicht direkt, sondern generiert nur Build-Steuerdateien aus CMakeLists.txt-Dateien. Die erste Veröffentlichung des Programms erfolgte im Jahr 2000. Zum Vergleich erschien der statische Analysator PVS-Studio erst 2008. Dann konzentrierte es sich darauf, Fehler beim Portieren von Programmen von 32-Bit-Systemen auf 64-Bit-Systeme zu finden, und 2010 erschien der erste Satz allgemeiner Diagnosen ( V501 - V545 ). Übrigens gibt es einige Warnungen von diesem ersten Satz im CMake-Code.

Unverzeihliche Fehler


V1040 Möglicher Tippfehler bei der Schreibweise eines vordefinierten Makronamens . Das Makro '__MINGW32_' ähnelt '__MINGW32__'. winapi.h 4112

/* from winternl.h */ #if !defined(__UNICODE_STRING_DEFINED) && defined(__MINGW32_) #define __UNICODE_STRING_DEFINED #endif 

Die V1040- Diagnose wurde erst kürzlich implementiert. Zum Zeitpunkt der Veröffentlichung des Artikels wird es höchstwahrscheinlich keine Veröffentlichung geben, aber mit Hilfe dieser Diagnose ist es uns bereits gelungen, einen schweren Fehler zu finden.

Hier machten sie einen Tippfehler im Namen __MINGW32_ . Am Ende fehlt ein Unterstrich. Wenn Sie nach Code mit diesem Namen suchen, können Sie sicherstellen, dass das Projekt tatsächlich die Version mit zwei Unterstrichen auf beiden Seiten verwendet:

Bild 3


V531 Es ist seltsam, dass ein Operator sizeof () mit sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 558

 bool IsVisualStudioMacrosFileRegistered(const std::string& macrosFile, const std::string& regKeyBase, std::string& nextAvailableSubKeyName) { .... if (ERROR_SUCCESS == result) { wchar_t subkeyname[256]; // <= DWORD cch_subkeyname = sizeof(subkeyname) * sizeof(subkeyname[0]); // <= wchar_t keyclass[256]; DWORD cch_keyclass = sizeof(keyclass) * sizeof(keyclass[0]); FILETIME lastWriteTime; lastWriteTime.dwHighDateTime = 0; lastWriteTime.dwLowDateTime = 0; while (ERROR_SUCCESS == RegEnumKeyExW(hkey, index, subkeyname, &cch_subkeyname, 0, keyclass, &cch_keyclass, &lastWriteTime)) { .... } .... } 

Wenn das Array statisch deklariert ist, berechnet der Operator sizeof seine Größe in Bytes, wobei sowohl die Anzahl der Elemente als auch die Größe der Elemente berücksichtigt werden. Bei der Berechnung des Werts der Variablen cch_subkeyname hat der Programmierer dies nicht berücksichtigt und einen viermal größeren Wert als geplant erhalten. Lassen Sie uns erklären, wo es "4 mal" ist.

Das Array und seine falsche Größe werden an die RegEnumKeyExW- Funktion übergeben:

 LSTATUS RegEnumKeyExW( HKEY hKey, DWORD dwIndex, LPWSTR lpName, // <= subkeyname LPDWORD lpcchName, // <= cch_subkeyname LPDWORD lpReserved, LPWSTR lpClass, LPDWORD lpcchClass, PFILETIME lpftLastWriteTime ); 

Der Zeiger lpcchName muss auf eine Variable zeigen, die die Größe des in Zeichen angegebenen Puffers enthält: "Ein Zeiger auf eine Variable, die die Größe des durch den Parameter lpClass angegebenen Puffers in Zeichen angibt." Das Subkeyname- Array hat eine Größe von 512 Byte und kann 256 Zeichen vom Typ wchar_t speichern (in Windows wchar_t sind es 2 Bytes). Dieser Wert ist 256 und sollte an die Funktion übergeben werden. Stattdessen wird 512 erneut mit 2 multipliziert, um 1024 zu erhalten.

Ich denke, wie man den Fehler behebt, ist jetzt klar. Verwenden Sie anstelle der Multiplikation die Division:

 DWORD cch_subkeyname = sizeof(subkeyname) / sizeof(subkeyname[0]); 

Übrigens tritt bei der Berechnung des Wertes der Variablen cch_keyclass genau der gleiche Fehler auf.

Der beschriebene Fehler kann möglicherweise zu einem Pufferüberlauf führen. Es ist notwendig, alle diese Stellen zu reparieren:

  • V531 Es ist seltsam, dass ein Operator sizeof () mit sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 556
  • V531 Es ist seltsam, dass ein Operator sizeof () mit sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 572
  • V531 Es ist seltsam, dass ein Operator sizeof () mit sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 621
  • V531 Es ist seltsam, dass ein Operator sizeof () mit sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 622
  • V531 Es ist seltsam, dass ein Operator sizeof () mit sizeof () multipliziert wird. cmGlobalVisualStudioGenerator.cxx 649

V595 Der Zeiger 'this-> BuildFileStream' wurde verwendet, bevor er gegen nullptr verifiziert wurde. Überprüfen Sie die Zeilen: 133, 134. cmMakefileTargetGenerator.cxx 133

 void cmMakefileTargetGenerator::CreateRuleFile() { .... this->BuildFileStream->SetCopyIfDifferent(true); if (!this->BuildFileStream) { return; } .... } 

Der this-> BuildFileStream-Zeiger wird unmittelbar vor der Validierungsprüfung dereferenziert. Hat das wirklich Probleme verursacht? Unten ist ein weiteres Beispiel für einen solchen Ort. Es wird direkt unter dem Kohlepapier hergestellt. Tatsächlich gibt es jedoch viele V595- Warnungen, von denen die meisten nicht so offensichtlich sind. Aus Erfahrung kann ich sagen, dass die Korrektur von Warnungen vor dieser Diagnose am längsten ist.

  • V595 Der Zeiger 'this-> FlagFileStream' wurde verwendet, bevor er gegen nullptr verifiziert wurde. Überprüfen Sie die Zeilen: 303, 304. cmMakefileTargetGenerator.cxx 303

V614 Nicht initialisierter Zeiger 'str' verwendet. cmVSSetupHelper.h 80

 class SmartBSTR { public: SmartBSTR() { str = NULL; } SmartBSTR(const SmartBSTR& src) { if (src.str != NULL) { str = ::SysAllocStringByteLen((char*)str, ::SysStringByteLen(str)); } else { str = ::SysAllocStringByteLen(NULL, 0); } } .... private: BSTR str; }; 

Der Analysator erkannte die Verwendung des nicht initialisierten Zeigers str . Und das entstand wegen des üblichen Tippfehlers. Beim Aufrufen der Funktion SysAllocStringByteLen mussten Sie den Zeiger src.str verwenden .

V557 Array-Überlauf ist möglich. Der Wert des 'Lensymbol'-Index könnte 28 erreichen. Archive_read_support_format_rar.c 2749

 static int64_t expand(struct archive_read *a, int64_t end) { .... if ((lensymbol = read_next_symbol(a, &rar->lengthcode)) < 0) goto bad_data; if (lensymbol > (int)(sizeof(lengthbases)/sizeof(lengthbases[0]))) goto bad_data; if (lensymbol > (int)(sizeof(lengthbits)/sizeof(lengthbits[0]))) goto bad_data; len = lengthbases[lensymbol] + 2; if (lengthbits[lensymbol] > 0) { if (!rar_br_read_ahead(a, br, lengthbits[lensymbol])) goto truncated_data; len += rar_br_bits(br, lengthbits[lensymbol]); rar_br_consume(br, lengthbits[lensymbol]); } .... } 

In diesem Code wurden mehrere Probleme gefunden. Beim Zugriff auf die Arrays von Längenbasen und Längenbits kann die Grenze des Arrays überschritten werden, da die Entwickler über dem Code den Operator '>' anstelle von '> =' geschrieben haben. Bei einer solchen Überprüfung wurde ein ungültiger Wert übersprungen. Wir sind mit einem klassischen Fehlermuster konfrontiert, das als Off-by-One-Fehler bezeichnet wird .

Die gesamte Liste der Orte, an denen über einen ungültigen Index auf Arrays zugegriffen werden kann:

  • V557 Array-Überlauf ist möglich. Der Wert des 'Lensymbol'-Index könnte 28 erreichen. Archive_read_support_format_rar.c 2750
  • V557 Array-Überlauf ist möglich. Der Wert des 'Lensymbol'-Index könnte 28 erreichen. Archive_read_support_format_rar.c 2751
  • V557 Array-Überlauf ist möglich. Der Wert des 'Lensymbol'-Index könnte 28 erreichen. Archive_read_support_format_rar.c 2753
  • V557 Array-Überlauf ist möglich. Der Wert des 'Lensymbol'-Index könnte 28 erreichen. Archive_read_support_format_rar.c 2754
  • V557 Array-Überlauf ist möglich. Der Wert des 'offssymbol'-Index könnte 60 erreichen. Archive_read_support_format_rar.c 2797

Speicherverlust


V773 Die Funktion wurde beendet, ohne den Zeiger 'testRun' loszulassen. Ein Speicherverlust ist möglich. cmCTestMultiProcessHandler.cxx 193

 void cmCTestMultiProcessHandler::FinishTestProcess(cmCTestRunTest* runner, bool started) { .... delete runner; if (started) { this->StartNextTests(); } } bool cmCTestMultiProcessHandler::StartTestProcess(int test) { .... cmCTestRunTest* testRun = new cmCTestRunTest(*this); // <= .... if (testRun->StartTest(this->Completed, this->Total)) { return true; // <= } this->FinishTestProcess(testRun, false); // <= return false; } 

Der Analysator hat einen Speicherverlust festgestellt. Speicher durch Zeiger testRun wird nicht freigegeben, wenn die Funktion testRun-> StartTest true zurückgibt. Wenn ein anderer Codezweig ausgeführt wird, wird der Speicher, der den testRun- Zeiger verwendet, in der Funktion this-> FinishTestProcess freigegeben .

Ressourcenleck


V773 Die Funktion wurde beendet, ohne die Datei zu schließen, auf die das Handle 'fd' verweist. Ein Ressourcenleck ist möglich. rhash.c 450

 RHASH_API int rhash_file(....) { FILE* fd; rhash ctx; int res; hash_id &= RHASH_ALL_HASHES; if (hash_id == 0) { errno = EINVAL; return -1; } if ((fd = fopen(filepath, "rb")) == NULL) return -1; if ((ctx = rhash_init(hash_id)) == NULL) return -1; // <= fclose(fd); ??? res = rhash_file_update(ctx, fd); fclose(fd); rhash_final(ctx, result); rhash_free(ctx); return res; } 

Seltsame Logik in Bedingungen


V590 Überprüfen Sie den Ausdruck '* s! =' \ 0 '&& * s ==' ''. Der Ausdruck ist übertrieben oder enthält einen Druckfehler. archive_cmdline.c 76

 static ssize_t get_argument(struct archive_string *as, const char *p) { const char *s = p; archive_string_empty(as); /* Skip beginning space characters. */ while (*s != '\0' && *s == ' ') s++; .... } 

Der Vergleich des Zeichens * s mit einer Endnull ist überflüssig. Der Zustand der while-Schleife hängt nur davon ab, ob das Zeichen einem Leerzeichen entspricht oder nicht. Dies ist kein Fehler, sondern eine zusätzliche Komplikation des Codes.

V592 Der Ausdruck wurde zweimal in Klammern gesetzt: ((Ausdruck)). Ein Klammerpaar ist nicht erforderlich oder es liegt ein Druckfehler vor. cmCTestTestHandler.cxx 899

 void cmCTestTestHandler::ComputeTestListForRerunFailed() { this->ExpandTestsToRunInformationForRerunFailed(); ListOfTests finalList; int cnt = 0; for (cmCTestTestProperties& tp : this->TestList) { cnt++; // if this test is not in our list of tests to run, then skip it. if ((!this->TestsToRun.empty() && std::find(this->TestsToRun.begin(), this->TestsToRun.end(), cnt) == this->TestsToRun.end())) { continue; } tp.Index = cnt; finalList.push_back(tp); } .... } 

Der Analysator warnt davor, dass die Negation möglicherweise in Klammern gesetzt werden sollte. Es scheint, dass es hier keinen solchen Fehler gibt - nur zusätzliche doppelte Klammern. Höchstwahrscheinlich liegt jedoch ein logischer Fehler in diesem Zustand vor.

Die continue- Anweisung wird ausgeführt, wenn die Liste der Tests this-> TestsToRun nicht leer ist und cnt nicht vorhanden ist. Es ist logisch anzunehmen, dass dieselbe Aktion ausgeführt werden muss, wenn die Testliste leer ist. Höchstwahrscheinlich sollte der Zustand folgendermaßen aussehen:

 if (this->TestsToRun.empty() || std::find(this->TestsToRun.begin(), this->TestsToRun.end(), cnt) == this->TestsToRun.end()) { continue; } 

V592 Der Ausdruck wurde zweimal in Klammern gesetzt: ((Ausdruck)). Ein Klammerpaar ist nicht erforderlich oder es liegt ein Druckfehler vor. cmMessageCommand.cxx 73

 bool cmMessageCommand::InitialPass(std::vector<std::string> const& args, cmExecutionStatus&) { .... } else if (*i == "DEPRECATION") { if (this->Makefile->IsOn("CMAKE_ERROR_DEPRECATED")) { fatal = true; type = MessageType::DEPRECATION_ERROR; level = cmake::LogLevel::LOG_ERROR; } else if ((!this->Makefile->IsSet("CMAKE_WARN_DEPRECATED") || this->Makefile->IsOn("CMAKE_WARN_DEPRECATED"))) { type = MessageType::DEPRECATION_WARNING; level = cmake::LogLevel::LOG_WARNING; } else { return true; } ++i; } .... } 

Ein ähnliches Beispiel, aber hier bin ich sicherer, wenn ein Fehler vorliegt. Die IsSet- Funktion ("CMAKE_WARN_DEPRECATED") überprüft, ob der Wert CMAKE_WARN_DEPRECATED global festgelegt ist, und die IsOn- Funktion ("CMAKE_WARN_DEPRECATED") überprüft, ob der Wert in der Projektkonfiguration angegeben ist. Höchstwahrscheinlich ist der Negationsoperator überflüssig, weil In beiden Fällen ist es richtig, die gleichen Typ- und Pegelwerte festzulegen.

V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. cmCTestRunTest.cxx 151

 bool cmCTestRunTest::EndTest(size_t completed, size_t total, bool started) { .... } else if ((success && !this->TestProperties->WillFail) || (!success && this->TestProperties->WillFail)) { this->TestResult.Status = cmCTestTestHandler::COMPLETED; outputStream << " Passed "; } .... } 

Ein solcher Code kann erheblich vereinfacht werden, indem der bedingte Ausdruck folgendermaßen umgeschrieben wird:

 } else if (success != this->TestProperties->WillFail) { this->TestResult.Status = cmCTestTestHandler::COMPLETED; outputStream << " Passed "; } 

Einige weitere Orte, die Sie vereinfachen können:

  • V728 Eine übermäßige Überprüfung kann vereinfacht werden. Die '(A && B) || (! A &&! B) 'Ausdruck entspricht dem Ausdruck' bool (A) == bool (B) '. cmCTestTestHandler.cxx 702
  • V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. Digest_sspi.c 443
  • V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. tcp.c 1295
  • V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. testDynamicLoader.cxx 58
  • V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. testDynamicLoader.cxx 65
  • V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. testDynamicLoader.cxx 72

Verschiedene Warnungen


V523 Die Anweisung 'then' entspricht dem nachfolgenden Codefragment. archive_read_support_format_ar.c 415

 static int _ar_read_header(struct archive_read *a, struct archive_entry *entry, struct ar *ar, const char *h, size_t *unconsumed) { .... /* * "__.SYMDEF" is a BSD archive symbol table. */ if (strcmp(filename, "__.SYMDEF") == 0) { archive_entry_copy_pathname(entry, filename); /* Parse the time, owner, mode, size fields. */ return (ar_parse_common_header(ar, entry, h)); } /* * Otherwise, this is a standard entry. The filename * has already been trimmed as much as possible, based * on our current knowledge of the format. */ archive_entry_copy_pathname(entry, filename); return (ar_parse_common_header(ar, entry, h)); } 

Der Ausdruck in der letzten Bedingung ist identisch mit den letzten beiden Zeilen der Funktion. Dieser Code kann durch Entfernen der Bedingung vereinfacht werden, oder es liegt ein Fehler im Code vor und sollte behoben werden.

V535 Die Variable 'i' wird für diese Schleife und für die äußere Schleife verwendet. Überprüfen Sie die Zeilen: 2220, 2241. multi.c 2241

 static CURLMcode singlesocket(struct Curl_multi *multi, struct Curl_easy *data) { .... for(i = 0; (i< MAX_SOCKSPEREASYHANDLE) && // <= (curraction & (GETSOCK_READSOCK(i) | GETSOCK_WRITESOCK(i))); i++) { unsigned int action = CURL_POLL_NONE; unsigned int prevaction = 0; unsigned int comboaction; bool sincebefore = FALSE; s = socks[i]; /* get it from the hash */ entry = sh_getentry(&multi->sockhash, s); if(curraction & GETSOCK_READSOCK(i)) action |= CURL_POLL_IN; if(curraction & GETSOCK_WRITESOCK(i)) action |= CURL_POLL_OUT; actions[i] = action; if(entry) { /* check if new for this transfer */ for(i = 0; i< data->numsocks; i++) { // <= if(s == data->sockets[i]) { prevaction = data->actions[i]; sincebefore = TRUE; break; } } } .... } 

Die Variable i wird als Schleifenzähler in den äußeren und verschachtelten Schleifen verwendet. In diesem Fall beginnt der Zählerwert im eingeschlossenen Eins erneut von Null an zu zählen. Dies ist hier möglicherweise kein Fehler, aber der Code ist verdächtig.

V519 Der Variablen 'tagString' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 84, 86. cmCPackLog.cxx 86

 oid cmCPackLog::Log(int tag, const char* file, int line, const char* msg, size_t length) { .... if (tag & LOG_OUTPUT) { output = true; display = true; if (needTagString) { if (!tagString.empty()) { tagString += ","; } tagString = "VERBOSE"; } } if (tag & LOG_WARNING) { warning = true; display = true; if (needTagString) { if (!tagString.empty()) { tagString += ","; } tagString = "WARNING"; } } .... } 

Die Variable tagString wird an allen Stellen durch den neuen Wert ausgefranst . Es ist schwer zu sagen, was der Fehler war oder warum sie es getan haben. Vielleicht waren die Operatoren '=' und '+ =' verwirrt.

Die ganze Liste solcher Orte:

  • V519 Der Variablen 'tagString' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 94, 96. cmCPackLog.cxx 96
  • V519 Der Variablen 'tagString' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 104, 106. cmCPackLog.cxx 106
  • V519 Der Variablen 'tagString' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 114, 116. cmCPackLog.cxx 116
  • V519 Der Variablen 'tagString' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 125, 127. cmCPackLog.cxx 127

V519 Der Variablen 'aes-> aes_set' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 4052, 4054. archive_string.c 4054

 int archive_mstring_copy_utf8(struct archive_mstring *aes, const char *utf8) { if (utf8 == NULL) { aes->aes_set = 0; // <= } aes->aes_set = AES_SET_UTF8; // <= .... return (int)strlen(utf8); } 

Das Erzwingen von AES_SET_UTF8 sieht verdächtig aus. Ich denke, ein solcher Code wird jeden Entwickler irreführen, der mit der Verfeinerung dieses Ortes konfrontiert ist.

Dieser Code wurde an eine weitere Stelle kopiert:

  • V519 Der Variablen 'aes-> aes_set' werden zweimal hintereinander Werte zugewiesen. Vielleicht ist das ein Fehler. Überprüfen Sie die Zeilen: 4066, 4068. archive_string.c 4068

So finden Sie Fehler in einem Projekt in CMake


In diesem Abschnitt werde ich Ihnen ein wenig erklären, wie Sie Projekte auf CMake mit PVS-Studio einfach und problemlos überprüfen können.

Windows / Visual Studio

Für Visual Studio können Sie die Projektdatei mithilfe der CMake-GUI oder des folgenden Befehls generieren:

 cmake -G "Visual Studio 15 2017 Win64" .. 

Als Nächstes können Sie die SLN-Datei öffnen und das Projekt mit dem Plug-In für Visual Studio testen.

Linux / MacOS

Auf diesen Systemen wird die Datei compile_commands.json verwendet, um das Projekt zu überprüfen. Es kann übrigens in verschiedenen Montagesystemen erzeugt werden. In CMake geschieht dies folgendermaßen:

 cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On .. 

Es bleibt, den Analysator im Verzeichnis mit der .json-Datei zu starten:

 pvs-studio-analyzer analyze -l /path/to/PVS-Studio.lic -o /path/to/project.log -e /path/to/exclude-path -j<N> 

Wir haben auch ein Modul für CMake-Projekte entwickelt. Einige Leute benutzen es gerne. Das CMake-Modul und Beispiele für seine Verwendung finden Sie in unserem Repository auf GitHub: pvs-studio-cmake-examples .

Fazit


Das große Publikum von CMake-Benutzern ist ein guter Tester des Projekts, aber viele Probleme konnten vor der Veröffentlichung mit Tools zur statischen Code-Analyse wie PVS-Studio nicht verhindert werden .

Wenn Ihnen die Ergebnisse des Analysators gefallen haben, Ihr Projekt jedoch nicht in C und C ++ geschrieben ist, möchte ich Sie daran erinnern, dass der Analysator auch die Analyse von Projekten in C # und Java unterstützt. Sie können den Analysator in Ihrem Projekt testen, indem Sie auf diese Seite gehen.



Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: Svyatoslav Razmyslov. CMake: Der Fall, in dem die Qualität des Projekts unverzeihlich ist .

Source: https://habr.com/ru/post/de464147/


All Articles