CMake жана C++ түбөлүктүү бир туугандар

CMake жана C++ түбөлүктүү бир туугандар

Өнүктүрүү учурунда мен компиляторлорду өзгөртүүнү, режимдерди курууну, көз карандылык версияларын, статикалык анализди жүргүзүүнү, өндүрүмдүүлүктү өлчөөнү, камтууларды чогултууну, документтерди түзүүнү ж. Жана мен чындап эле CMakeди жакшы көрөм, анткени ал мага каалаган нерсени жасоого мүмкүндүк берет.

Көптөгөн адамдар CMakeди сындашат жана көп учурда татыктуу, бирок аны карасаңыз, баары жаман эмес жана жакында такыр жаман эмес, жана өнүгүү багыты кыйла оң.

Бул эскертүүдө, мен сизге CMake системасында C++ тилинде баш китепкананы кантип уюштурууну айткым келет, бул төмөнкү функцияларды алуу үчүн:

  1. Ассамблея;
  2. Autorun тесттери;
  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_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Ассамблея максаттары
      1. демейки боюнча
      2. mylib-бирдик-тесттер
      3. текшерүү
      4. чагылдыруу
      5. док
      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 (Cm. 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_COVERAGE

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 берилгенге. Жеткиликтүү тилдердин тизмеси үчүн караңыз Doxygen системасы сайты.

Орус тили демейки боюнча иштетилген.

Ассамблея максаттары

демейки боюнча

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.

док

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 куруу системасын коомдун жана ден соолуктун жыргалчылыгы үчүн колдонуңуз.

Долбоордун шаблонун жүктөп алыңыз

Source: www.habr.com

Комментарий кошуу