CMake және C++ бір-біріне мәңгі бауырлас

CMake және C++ бір-біріне мәңгі бауырлас

Әзірлеу барысында мен компиляторларды өзгертуді, режимдерді құруды, тәуелділік нұсқаларын жасауды, статикалық талдауды орындауды, өнімділікті өлшеуді, қамтуды жинауды, құжаттаманы жасауды және т.б. Мен CMake-ді шынымен жақсы көремін, өйткені ол маған қалағанның бәрін жасауға мүмкіндік береді.

Көптеген адамдар CMake-ді сынайды, және көбінесе бұл лайықты, бірақ егер сіз оған қарасаңыз, бәрі соншалықты жаман емес, және жақында мүлде жаман емес, ал даму бағыты айтарлықтай оңды.

Бұл жазбада мен келесі функцияларды алу үшін CMake жүйесінде C++ тілінде тақырып кітапханасын жай қалай ұйымдастыру керектігін айтқым келеді:

  1. Ассамблея;
  2. Автоматты іске қосу сынақтары;
  3. Кодты қамтуды өлшеу;
  4. Орнату;
  5. Автоқұжаттау;
  6. Онлайн құм жәшігін құру;
  7. Статикалық талдау.

Артықшылықтарды және C-жасауды түсінетін кез келген адам оңай жасай алады жоба үлгісін жүктеп алыңыз және оны пайдалануды бастаңыз.


Мазмұны

  1. Ішінен жоба
    1. Жоба құрылымы
    2. Негізгі CMake файлы (./CMakeLists.txt)
      1. Жоба туралы ақпарат
      2. Жоба опциялары
      3. Компиляция опциялары
      4. Негізгі мақсат
      5. параметр
      6. Тесттер
      7. жазбалар
      8. Онлайн құм жәшігі
    3. Сынақ сценарийі (test/CMakeLists.txt)
      1. Тестілеу
      2. Қаптау
    4. Құжаттамаға арналған сценарий (doc/CMakeLists.txt)
    5. Онлайн құм жәшігіне арналған сценарий (online/CMakeLists.txt)
  2. Сырттағы жоба
    1. Ассамблея
      1. Ұрпақ
      2. Ассамблея
    2. Опциялар
      1. MYLIB_ҚАМТУ
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Құрастыру мақсаттары
      1. Әдепкі бойынша
      2. mylib-бірлік сынақтары
      3. тексеру
      4. қамту
      5. Doc
      6. таяқша жәшігі
    4. мысалдар
  3. Құралдар
  4. Статикалық талдау
  5. Кейінгі сөз

Ішінен жоба

Жоба құрылымы

.
├── CMakeLists.txt
├── README.en.md
├── README.md
├── doc
│   ├── CMakeLists.txt
│   └── Doxyfile.in
├── include
│   └── mylib
│       └── myfeature.hpp
├── online
│   ├── CMakeLists.txt
│   ├── mylib-example.cpp
│   └── wandbox.py
└── test
    ├── CMakeLists.txt
    ├── mylib
    │   └── myfeature.cpp
    └── test_main.cpp

Біз негізінен CMake сценарийлерін қалай ұйымдастыру керектігі туралы айтатын боламыз, сондықтан олар егжей-тегжейлі талқыланады. Қалған файлдарды кез келген адам тікелей көре алады үлгі жобасы бетінде.

Негізгі CMake файлы (./CMakeLists.txt)

Жоба туралы ақпарат

Ең алдымен, CMake жүйесінің қажетті нұсқасын сұрау керек. CMake дамып келеді, пәрмен қолтаңбалары және әртүрлі жағдайларда мінез-құлық өзгереді. CMake біздің одан не қалайтынымызды бірден түсінуі үшін біз оған қойылатын талаптарымызды дереу жазып алуымыз керек.

cmake_minimum_required(VERSION 3.13)

Содан кейін біз жобамызды, оның атауын, нұсқасын, қолданылатын тілдерді және т.б. белгілейміз (қараңыз. команду project).

Бұл жағдайда біз тілді көрсетеміз CXX (және бұл C++ дегенді білдіреді), сондықтан CMake Си тілінің компиляторын шиеленістірмейді және іздемейді (әдепкі бойынша CMake екі тілді қамтиды: C және C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Мұнда сіз біздің жобаның қосалқы жоба ретінде басқа жобаға қосылғанын бірден тексере аласыз. Бұл болашақта көп көмегін тигізеді.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Жоба опциялары

Біз екі нұсқаны береміз.

Бірінші нұсқа MYLIB_TESTING — бірлік сынақтарын өшіру. Бұл сынақтарға бәрі сәйкес келетініне сенімді болсақ, қажет болуы мүмкін, бірақ біз, мысалы, жобамызды орнатуды немесе пакеттеуді ғана қалаймыз. Немесе біздің жоба қосалқы жоба ретінде енгізілген - бұл жағдайда жобамыздың пайдаланушысы біздің сынақтарымызды орындауға мүдделі емес. Сіз пайдаланатын тәуелділіктерді сынамайсыз, солай ма?

option(MYLIB_TESTING "Включить модульное тестирование" ON)

Сонымен қатар, біз бөлек опцияны жасаймыз MYLIB_COVERAGE сынақтар арқылы кодты қамтуды өлшеу үшін, бірақ ол қосымша құралдарды қажет етеді, сондықтан оны анық қосу керек.

option(MYLIB_COVERAGE "Включить измерение покрытия кода тестами" OFF)

Компиляция опциялары

Әрине, біз керемет бағдарламашылармыз, сондықтан компилятордан компиляция уақыты диагностикасының максималды деңгейін қалаймыз. Бірде-бір тышқан өтіп кетпейді.

add_compile_options(
    -Werror

    -Wall
    -Wextra
    -Wpedantic

    -Wcast-align
    -Wcast-qual
    -Wconversion
    -Wctor-dtor-privacy
    -Wenum-compare
    -Wfloat-equal
    -Wnon-virtual-dtor
    -Wold-style-cast
    -Woverloaded-virtual
    -Wredundant-decls
    -Wsign-conversion
    -Wsign-promo
)

C++ тіл стандартына толық сәйкес болу үшін кеңейтімдерді де өшіреміз. Олар CMake ішінде әдепкі бойынша қосылады.

if(NOT CMAKE_CXX_EXTENSIONS)
    set(CMAKE_CXX_EXTENSIONS OFF)
endif()

Негізгі мақсат

Біздің кітапхана тек тақырыптық файлдардан тұрады, яғни бізде статикалық немесе динамикалық кітапханалар түріндегі шығыстар жоқ. Екінші жағынан, кітапханамызды сырттан пайдалану үшін оны орнату керек, ол жүйеде анықталып, жобаңызға қосылған болуы керек, сонымен қатар дәл сол тақырыптар, сонымен қатар кейбір қосымшалар, қасиеттері оған бекітіледі.

Осы мақсатта біз интерфейс кітапханасын жасаймыз.

add_library(mylib INTERFACE)

Біз тақырыптарды интерфейс кітапханасына байланыстырамыз.

CMake-ті заманауи, сәнді, жастар қолдануы тақырыптарды, қасиеттерді және т.б. бір нысана арқылы беріледі. Сондықтан айтсақ та жеткілікті target_link_libraries(target PRIVATE dependency), және мақсатпен байланыстырылған барлық тақырыптар dependency, мақсатқа жататын көздер үшін қолжетімді болады target. Ал сізге ешқайсысы қажет емес [target_]include_directories. Бұл төменде талдауда көрсетіледі Бірлік сынақтары үшін CMake сценарийі.

Сондай-ақ деп аталатындарға назар аударған жөн. выражения-генераторы: $<...>.

Бұл пәрмен бізге қажет тақырыптарды интерфейс кітапханасымен байланыстырады және біздің кітапхана бірдей CMake иерархиясындағы кез келген мақсатқа қосылған болса, каталогтағы тақырыптар онымен байланыстырылады. ${CMAKE_CURRENT_SOURCE_DIR}/include, және егер біздің кітапхана жүйеде орнатылған болса және пәрмен арқылы басқа жобаға қосылған болса find_package, содан кейін каталогтағы тақырыптар онымен байланыстырылады include орнату каталогына қатысты.

target_include_directories(mylib INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

Тіл стандартын белгілейік. Әрине, ең соңғысы. Сонымен бірге біз стандартты енгізіп қана қоймай, оны кітапханамызды пайдаланатындарға да таратамыз. Бұл жиынтық сипаттың категорияға ие болуына байланысты қол жеткізіледі INTERFACE (қара. target_compile_features командасы).

target_compile_features(mylib INTERFACE cxx_std_17)

Кітапханамызға бүркеншік ат жасап көрейік. Оның үстіне сұлулық үшін ол арнайы «аттар кеңістігінде» болады. Бұл біздің кітапханада әртүрлі модульдер пайда болған кезде пайдалы болады және біз оларды бір-бірінен тәуелсіз қосуға барамыз. Мысалы, Бустадағы сияқты.

add_library(Mylib::mylib ALIAS mylib)

параметр

Жүйеге тақырыптарымызды орнату. Мұнда бәрі қарапайым. Біз барлық тақырыптары бар қалта каталогқа кіруі керек деп айтамыз include орнату орнына қатысты.

install(DIRECTORY include/mylib DESTINATION include)

Әрі қарай, біз құрастыру жүйесіне үшінші тарап жобаларында пәрменді шақыру мүмкіндігіміз туралы хабарлаймыз find_package(Mylib) және мақсатқа жету Mylib::mylib.

install(TARGETS mylib EXPORT MylibConfig)
install(EXPORT MylibConfig NAMESPACE Mylib:: DESTINATION share/Mylib/cmake)

Келесі заклинанияны осылай түсіну керек. Үшінші тарап жобасында біз пәрменді шақырамыз find_package(Mylib 1.2.3 REQUIRED), ал орнатылған кітапхананың нақты нұсқасы нұсқамен үйлеспейтін болады 1.2.3CMake автоматты түрде қате жасайды. Яғни нұсқаларды қолмен қадағалаудың қажеті болмайды.

include(CMakePackageConfigHelpers)
write_basic_package_version_file("${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake"
    VERSION
        ${PROJECT_VERSION}
    COMPATIBILITY
        AnyNewerVersion
)
install(FILES "${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake" DESTINATION share/Mylib/cmake)

Тесттер

Егер сынақтар ашық түрде өшірілсе сәйкес опция немесе біздің жоба қосалқы жоба болып табылады, яғни пәрмен арқылы басқа CMake жобасына қосылған add_subdirectory, біз иерархия бойынша әрі қарай жылжымаймыз және сынақтарды жасау және іске қосу пәрмендерін сипаттайтын сценарий жай орындалмайды.

if(NOT MYLIB_TESTING)
    message(STATUS "Тестирование проекта Mylib выключено")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib не тестируется в режиме подмодуля")
else()
    add_subdirectory(test)
endif()

жазбалар

Құжаттама кіші жоба жағдайында да жасалмайды.

if(NOT IS_SUBPROJECT)
    add_subdirectory(doc)
endif()

Онлайн құм жәшігі

Сол сияқты ішкі жобада да онлайн құм жәшігі болмайды.

if(NOT IS_SUBPROJECT)
    add_subdirectory(online)
endif()

Сынақ сценарийі (test/CMakeLists.txt)

Тестілеу

Ең алдымен, біз қажетті сынақ жүйесі бар пакетті табамыз (таңдаулысымен ауыстырыңыз).

find_package(doctest 2.3.3 REQUIRED)

Тесттермен орындалатын файлымызды жасайық. Әдетте, тікелей орындалатын екілік файлға функцияны қамтитын файлды ғана қосамын main.

add_executable(mylib-unit-tests test_main.cpp)

Мен сынақтардың өздері кейінірек сипатталатын файлдарды қосамын. Бірақ мұны істеудің қажеті жоқ.

target_sources(mylib-unit-tests PRIVATE mylib/myfeature.cpp)

Біз тәуелділіктерді байланыстырамыз. Біз екілік жүйеге қажет CMake мақсаттарын ғана байланыстырғанымызды және пәрменді шақырмағанымызды ескеріңіз target_include_directories. Сынақ шеңберінен және біздікінен айдарлар Mylib::mylib, сондай-ақ құрастыру параметрлері (біздің жағдайда бұл C++ тіл стандарты) осы мақсаттармен бірге келді.

target_link_libraries(mylib-unit-tests
    PRIVATE
        Mylib::mylib
        doctest::doctest
)

Соңында біз «құрылымы» орындалатын сынақтарға тең келетін жалған мақсатты жасаймыз және бұл мақсатты әдепкі құрастыруға қосамыз (атрибут бұған жауапты ALL). Бұл әдепкі құрастыру сынақтарды іске қосу үшін іске қосатынын білдіреді, яғни біз оларды іске қосуды ешқашан ұмытпаймыз.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Қаптау

Әрі қарай, сәйкес опция көрсетілген болса, кодты қамтуды өлшеуді қосамыз. Мен егжей-тегжейге тоқталмаймын, өйткені олар CMake-ге қарағанда қамтуды өлшеу құралына көбірек қатысты. Нәтижелер негізінде мақсат құрылатынын атап өту маңызды coverage, оның көмегімен қамтуды өлшеуді бастау ыңғайлы.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Измерение покрытия кода тестами включено")

    target_compile_options(mylib-unit-tests PRIVATE --coverage)
    target_link_libraries(mylib-unit-tests PRIVATE gcov)

    add_custom_target(coverage
        COMMAND
            ${GCOVR_EXECUTABLE}
                --root=${PROJECT_SOURCE_DIR}/include/
                --object-directory=${CMAKE_CURRENT_BINARY_DIR}
        DEPENDS
            check
    )
elseif(MYLIB_COVERAGE AND NOT GCOVR_EXECUTABLE)
    set(MYLIB_COVERAGE OFF)
    message(WARNING "Для замеров покрытия кода тестами требуется программа gcovr")
endif()

Құжаттамаға арналған сценарий (doc/CMakeLists.txt)

Доксиген табылды.

find_package(Doxygen)

Әрі қарай, пайдаланушы тіл айнымалысын орнатқанын тексереміз. Олай болса, тиіспейміз, жоқ болса, орыс тілін аламыз. Содан кейін біз Doxygen жүйелік файлдарын конфигурациялаймыз. Барлық қажетті айнымалылар, соның ішінде тіл, конфигурациялау процесі кезінде сол жерге барады (қараңыз. команду configure_file).

Содан кейін біз мақсат жасаймыз doc, ол құжаттаманы жасай бастайды. Құжаттаманы жасау әзірлеу процесіндегі ең үлкен қажеттілік болмағандықтан, мақсат әдепкі бойынша қосылмайды; ол нақты іске қосылуы керек.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Russian)
    endif()
    message(STATUS "Doxygen documentation will be generated in ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

Онлайн құм жәшігіне арналған сценарий (online/CMakeLists.txt)

Мұнда біз үшінші Python табамыз және мақсатты жасаймыз wandbox, ол API сервисіне сәйкес сұрауды жасайды Таяқша жәшігі, және оны жібереді. Жауап дайын құм жәшігіне сілтемемен бірге келеді.

find_program(PYTHON3_EXECUTABLE python3)
if(PYTHON3_EXECUTABLE)
    set(WANDBOX_URL "https://wandbox.org/api/compile.json")

    add_custom_target(wandbox
        COMMAND
            ${PYTHON3_EXECUTABLE} wandbox.py mylib-example.cpp "${PROJECT_SOURCE_DIR}" include |
            curl -H "Content-type: application/json" -d @- ${WANDBOX_URL}
        WORKING_DIRECTORY
            ${CMAKE_CURRENT_SOURCE_DIR}
        DEPENDS
            mylib-unit-tests
    )
else()
    message(WARNING "Для создания онлайн-песочницы требуется интерпретатор ЯП python 3-й версии")
endif()

Сырттағы жоба

Енді осының бәрін қалай пайдалану керектігін қарастырайық.

Ассамблея

Бұл жобаны құру, CMake құрастыру жүйесіндегі кез келген басқа жоба сияқты, екі кезеңнен тұрады:

Ұрпақ

cmake -S путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Жоғарыдағы пәрмен CMake ескі нұсқасына байланысты жұмыс істемесе, өткізіп көріңіз -S:

cmake путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Опциялар туралы толығырақ.

Жобаны құру

cmake --build путь/к/сборочной/директории [--target target]

Жиналыстың мақсаттары туралы толығырақ.

Опциялар

MYLIB_ҚАМТУ

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [прочие опции ...]

Мақсатты қамтиды coverage, оның көмегімен сынақтар арқылы кодты қамтуды өлшеуді бастауға болады.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [прочие опции ...]

Бірлік сынағын құрастыруды және мақсатты өшіру мүмкіндігін береді check. Нәтижесінде сынақтармен кодты қамтуды өлшеу өшіріледі (қараңыз. MYLIB_COVERAGE).

Сондай-ақ, жоба пәрмен арқылы қосалқы жоба ретінде басқа жобаға қосылған болса, тестілеу автоматты түрде өшіріледі add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [прочие опции ...]

Мақсат жасайтын құжаттама тілін ауыстырады doc берілгенге. Қол жетімді тілдер тізімін көру үшін қараңыз Доксиген жүйесінің веб-сайты.

Орыс тілі әдепкі бойынша қосылған.

Құрастыру мақсаттары

Әдепкі бойынша

cmake --build path/to/build/directory
cmake --build path/to/build/directory --target all

Егер мақсат көрсетілмесе (ол мақсатқа тең all), қолынан келгеннің бәрін жинайды, сонымен қатар нысананы шақырады check.

mylib-бірлік сынақтары

cmake --build path/to/build/directory --target mylib-unit-tests

Бірлік сынақтарын құрастырады. Әдепкі бойынша қосылған.

тексеру

cmake --build путь/к/сборочной/директории --target check

Жиналған (жинақталған, егер әлі жоқ болса) бірлік сынақтарын іске қосады. Әдепкі бойынша қосылған.

Сондай-ақ қараңыз mylib-unit-tests.

қамту

cmake --build путь/к/сборочной/директории --target coverage

Бағдарламаны қолданатын сынақтар арқылы кодты қамту үшін орындалатын (егер іске қосылмаған болса) бірлік сынақтарын талдайды gcovr.

Жабылатын сорғыш келесідей болады:

------------------------------------------------------------------------------
                           GCC Code Coverage Report
Directory: /path/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
TOTAL                                          2       2   100%
------------------------------------------------------------------------------

Мақсат опция қосылғанда ғана қолжетімді болады MYLIB_COVERAGE.

Сондай-ақ қараңыз check.

Doc

cmake --build путь/к/сборочной/директории --target doc

Жүйені пайдаланып кодтық құжаттаманы генерациялауды бастайды Қышқыл.

таяқша жәшігі

cmake --build путь/к/сборочной/директории --target wandbox

Қызметтен жауап келесідей көрінеді:

{
    "permlink" :    "QElvxuMzHgL9fqci",
    "status" :  "0",
    "url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}

Бұл үшін қызмет пайдаланылады Таяқша жәшігі. Мен олардың серверлері қаншалықты икемді екенін білмеймін, бірақ бұл мүмкіндікті теріс пайдаланбау керек деп ойлаймын.

мысалдар

Жобаны қамтуды өлшеу арқылы жөндеу режимінде жасаңыз

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build путь/к/сборочной/директории --target coverage --parallel 16

Жобаны алдын ала құрастырусыз және сынаусыз орнату

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=путь/к/установойной/директории
cmake --build путь/к/сборочной/директории --target install

Берілген компилятормен босату режимінде құрастырыңыз

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=путь/к/директории/куда/установлены/зависимости
cmake --build путь/к/сборочной/директории --parallel 4

Ағылшын тілінде құжаттама жасау

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build путь/к/сборочной/директории --target doc

Құралдар

  1. CMake 3.13

    Шын мәнінде, CMake 3.13 нұсқасы осы анықтамада сипатталған кейбір консоль пәрмендерін орындау үшін ғана қажет. CMake сценарийлерінің синтаксисі тұрғысынан, егер генерация басқа жолмен шақырылса, 3.8 нұсқасы жеткілікті.

  2. Сынақ кітапханасы доктест

    Тестілеуді өшіруге болады (қараңыз опцию MYLIB_TESTING).

  3. Қышқыл

    Құжаттама жасалатын тілді ауыстыру үшін опция берілген MYLIB_DOXYGEN_LANGUAGE.

  4. Тіл аудармашысы Python 3

    Автоматты генерациялау үшін онлайн құм жәшіктері.

Статикалық талдау

CMake және бірнеше жақсы құралдардың көмегімен сіз аз күш жұмсай отырып, статикалық талдауды қамтамасыз ете аласыз.

Cppcheck

CMake статикалық талдау құралы үшін кірістірілген қолдауға ие Cppcheck.

Мұны істеу үшін опцияны пайдалану керек CMAKE_CXX_CPPCHECK:

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Iпуть/к/исходникам/include"

Осыдан кейін статикалық талдау дереккөзді құрастырған және қайта құрастырған сайын автоматты түрде іске қосылады. Қосымша ештеңе жасаудың қажеті жоқ.

Сыңғырлау

Керемет құралдың көмегімен scan-build Сондай-ақ, статикалық талдауды қысқа мерзімде іске қосуға болады:

scan-build cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build путь/к/сборочной/директории

Мұнда, Cppcheck жағдайынан айырмашылығы, құрастыруды әр жолы іске қосу керек scan-build.

Кейінгі сөз

CMake - кез келген дәм мен түс үшін функционалдылықты жүзеге асыруға мүмкіндік беретін өте қуатты және икемді жүйе. Ал, синтаксис кейде қалаусыз қалдырса да, шайтан әлі де ол боялғандай қорқынышты емес. CMake құрастыру жүйесін қоғам мен денсаулық үшін пайдаланыңыз.

Жоба үлгісін жүктеп алыңыз

Ақпарат көзі: www.habr.com

пікір қалдыру