CMake ja C++ — vennad igavesti

CMake ja C++ — vennad igavesti

Arenduse kĂ€igus meeldib mulle vahetada kompilaatoreid, koostamisreĆŸiime, sĂ”ltuvuste versioone, teostada staatilist analĂŒĂŒsi, mÔÔta jĂ”udlust, koguda katvust, genereerida dokumentatsiooni jne. Ja mulle vĂ€ga meeldib CMake, sest see vĂ”imaldab mul teha kĂ”ike seda, mida ma tahan.

Paljud kritiseerivad CMake’i ja tihti Ă”igustatult, kuid kui sĂŒveneda, siis ei ole kĂ”ik nii hull ja viimasel ajal isegi ĂŒsna hĂ€sti, ning arengu suund on igati positiivne.

KĂ€esolevas mĂ€rkuses tahan rÀÀkida, kuidas on ĂŒsna lihtne korraldada C++ keeles pealkirjafaili CMake sĂŒsteemis, et saavutada jĂ€rgmine funktsionaalsus:

  1. Koostamine;
  2. Automaatne testide kÀivitamine;
  3. Koodikatvuse mÔÔtmine;
  4. Installeerimine;
  5. Autodokumenteerimine;
  6. Veebihoiuruumi genereerimine;
  7. Staatiline analĂŒĂŒs.

Kes juba teab plussidest ja CMake’ist, vĂ”ib lihtsalt alla laadida projekti malli ja hakata seda kasutama.


Sisu

  1. Projekt seestpoolt
    1. Projekti struktuur
    2. Peamine CMake fail (./CMakeLists.txt)
      1. Projektiteave
      2. Projekti valikud
      3. Kompilatsiooni valikud
      4. Peamine siht
      5. Installeerimine
      6. Testid
      7. Dokumentatsioon
      8. Veebihoiuruum
    3. Testide skript (test/CMakeLists.txt)
      1. Testimine
      2. Katvus
    4. Dokumentatsiooni skript (doc/CMakeLists.txt)
    5. Veebihoiuruumi skript (online/CMakeLists.txt)
  2. Projekt vÀljastpoolt
    1. Koostamine
      1. Generatsioon
      2. Koostamine
    2. Valikud
      1. MYLIB_KATTE
      2. MYLIB_TESTIMINE
      3. MYLIB_DOXYGEN_VEEBIMISKEEL
    3. Kogumis eesmÀrgid
      1. Vaikimisi
      2. mylib-unit-tests
      3. kontrolli
      4. coverage
      5. dok
      6. wandbox
    4. NĂ€ited
  3. Tööriistad
  4. Statiline analĂŒĂŒs
  5. JÀrelsÔna

Projekt seestpoolt

Projekti struktuur

.
├── 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

Peamiselt kĂ€sitletakse, kuidas organiseerida CMake-skripte, seega uurime neid ĂŒksikasjalikult. ÜlejÀÀnud faile saavad huvilised otseselt vaadata projekti-malli lehelt.

Peamine CMake fail (./CMakeLists.txt)

Projektiteave

Esiteks tuleb kĂŒsida vajaliku CMake sĂŒsteemi versiooni. CMake areneb, muutuvad kĂ€skude signatuurid ja kĂ€itumine erinevates tingimustes. Et CMake mĂ”istaks kohe, mida me temalt tahame, peame kohe fikseerima meie nĂ”udmised.

cmake_minimum_required(VERSION 3.13)

SeejÀrel mÀÀratleme meie projekti, selle nime, versiooni, kasutatavad keeled ja muud (vt. kÀsku project).

Selles olukorras mÀÀrame keele CXX (mis tÀhendab C++), et CMake ei peaks muretsema ja ei peaks otsima C keele kompilaatorit (CMake sisaldab vaikimisi kahte keelt: C ja C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Siit saate kohe kontrollida, kas meie projekt on mÔnes teises projektis alamprojekti kujul. See aitab edaspidi palju.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Projekti valikud

Arvutame vÀlja kaks valikut.

Esimene valik — MYLIB_TESTIMINE — moodultestide vĂ€lja lĂŒlitamiseks. See vĂ”ib olla vajalik, kui oleme kindlad, et testid on korras ja soovime nĂ€iteks ainult oma projekti installida vĂ”i paketitada. VĂ”i on meie projekt alamprojektina kaasatud — sel juhul ei ole meie projekti kasutaja huvitatud meie testide kĂ€ivitamisest. Kas te ei testeeri sĂ”ltuvusi, mida kasutate?

option(MYLIB_TESTING "Aktiveeri moodulitestimine" ON)

Lisaks teeme eraldi valiku MYLIB_KATTE testikattete mÔÔtmiseks, kuid see vajab lisatööriistu, seega tuleb see selgesÔnaliselt aktiveerida.

option(MYLIB_COVERAGE "Aktiveeri testikattete mÔÔtmine" OFF)

Kompilatsiooni valikud

Muidugi oleme me lahedad C++ programmeerijad, seega soovime kompilatsioonivaatleja maksimaalset diagnoosimist kompilatsiooni ajal. Ükski hiir ei pÀÀse lĂ€bi.

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
)

Laieltame laiendused vĂ€lja, et tĂ€ielikult vastata C++ keele standardile. CMake'is on need vaikimisi sisse lĂŒlitatud.

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

Peamine siht

Meie teek koosneb ainult pĂ€istefailidest, seega ei ole meil mingit vĂ€ljundit staatiliste vĂ”i dĂŒnaamiliste teekide nĂ€ol. Teisest kĂŒljest, et kasutada meie teeki vĂ€ljastpoolt, tuleb see installida, et see oleks sĂŒsteemis avastatav ja seostatav oma projektiga, samas koos kaasnevate pĂ€iste ning vĂ”imalik, et ka mĂ”ne tĂ€iendava omadusega.

Selle eesmÀrgi saavutamiseks loome liidese teegi.

add_library(mylib INTERFACE)

Seome pÀised meie liidese teegiga.

Kaasaegne, trendikas ja noortele suunatud CMake'i kasutamine eeldab, et pĂ€ised, omadused jt edastatakse lĂ€bi ĂŒheainsa sihisse. Seega piisab öelda target_link_libraries(target PRIVATE dependency), ja kĂ”ik pealkirjad, mis on seotud eesmĂ€rgiga sĂ”ltuvus, on saadaval allikate jaoks, mis kuuluvad eesmĂ€rgile target. Ja mingeid ei nĂ”uta [target_]include_directories. Seda demonstreeritakse allpool, kui kĂ€sitletakse CMake-skripti modulaarsete testide jaoks.

Samuti tasub tÀhele panna nn avaldis-jaanid: $.

See kĂ€sk seondab vajalikud pealkirjad meie liidesti raamatukoguga; kui meie raamatukogu on ĂŒhendatud mĂ”ne eesmĂ€rgiga sama CMake hierarhia raames, siis seondatakse selle liidese pealkirjad direktorist ${CMAKE_CURRENT_SOURCE_DIR}/include, ja kui meie raamatukogu on installitud sĂŒsteemi ja ĂŒhendatud teises projektis kĂ€suga find_package, siis seondatakse selle pealkirjad direktorist include installatsiooni direktorist.

target_include_directories(mylib INTERFACE
    $
    $
)

Seame keele standardi. Loomulikult, kÔige uuema. Samuti mitte lihtsalt standardi kaasamine, vaid ka selle laiendamine nendele, kes meie raamatukogu kasutavad. See saavutatakse seetÔttu, et seatud omadus kuulub kategooriasse INTERFACE (vt. komandat target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Loome alias meie teegile. Ja kauni vĂ€ljanĂ€gemise nimel asetame selle spetsiaalsesse „nimetĂŒkki“. See on kasulik, kui meie teegis ilmuvad erinevad moodulid, ja me saame neid iseseisvalt lisada. Nagu Boostis nĂ€iteks.

add_library(Mylib::mylib ALIAS mylib)

Installeerimine

Meie pĂ€isefailide installimine sĂŒsteemi. Siin on kĂ”ik lihtne. Ütleme, et kaust, kus kĂ”ik pĂ€isefailid asuvad, peab jĂ”udma installatsioonikausta include installatsiooni asukoha suhtes.

install(DIRECTORY include/mylib DESTINATION include)

SeejĂ€rel teavitame ehitussĂŒsteemi, et soovime, et vĂ”ime kolmandates projektides kutsuda kĂ€sklust find_package(Mylib) ja saada eesmĂ€rgi Mylib::mylib.

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

JĂ€rgmine peatĂŒkk tuleb mĂ”ista nii. Kui kolmandas projektis kutsume kĂ€skluse find_package(Mylib 1.2.3 REQUIRED), ja samas osutub installitud teegi tegelik versioon mitte ĂŒhilduv versiooniga 1.2.3, genereerib CMake automaatselt vea. See tĂ€hendab, et versioonide jĂ€lgimisega ei pea enam kĂ€sitsi tegelema.

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)

Testid

Kui testid on vĂ€lja lĂŒlitatud vastava valiku abil vĂ”i meie projekt on alaprojekt, st see on seotud teise CMake-projektiga kĂ€sklusega add_subdirectory, siis me ei jĂ€tka hierarhias edasi ning skript, milles on kirjas testide genereerimise ja kĂ€itamise kĂ€sklused, ei kĂ€ivitu.

if(NOT MYLIB_TESTING)
    message(STATUS "Mylibi projekti testimine on vĂ€lja lĂŒlitatud")
elseif(IS_SUBPROJECT)
    message(STATUS "Mylib ei testita alamooduli reĆŸiimis")
else()
    add_subdirectory(test)
endif()

Dokumentatsioon

Dokumentatsiooni ei genereerita samuti alaprojekti puhul.

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

Veebihoiuruum

Sarnaselt ei ole alaprojektil ka veebihÔlvijad.

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

Testide skript (test/CMakeLists.txt)

Testimine

Esmalt leiame paketi vajaliku testimisraamistiku jaoks (asendage oma lemmikuga).

find_package(doctest 2.3.3 REQUIRED)

Loomeme meie tÀitmisfail testide jaoks. Tavalise tÀitmise binaari lisan ma tavaliselt ainult faili, kus on funktsioon main.

add_executable(mylib-unit-tests test_main.cpp)

Failid, kus testid on kirjeldatud, lisan hiljem. Kuid see pole kohustuslik.

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

Ühendame sĂ”ltuvused. Pange tĂ€hele, et meie binaarprogrammi oleme sidunud vaid vajalike CMake-eesmĂ€rkidega, ja ei kĂ€ivitanud kĂ€sku target_include_directories. Testimisraamistiku ja meie Mylib::mylib, samuti kogumispĂ”himĂ”tted (meie puhul on see keelestandardi C++) lĂ€ksid koos nende eesmĂ€rkidega.

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

LÔpuks loome vale eesmÀrgi, mille "kogumine" on vÔrdne testide kÀivitamisega, ja lisame selle eesmÀrgi vaikimisi kogumisse (selle eest vastutab atribuut ALL). See tÀhendab, et vaikimisi kogumine kÀivitab testide kÀivitamise, seega me kunagi ei unusta neid kÀivitada.

add_custom_target(check ALL COMMAND mylib-unit-tests)

Katvus

JĂ€rgmisena lĂŒlitame sisse koodi katvuse mÔÔtmise, kui vastav valik on mÀÀratud. Üksikasjadesse ei lasku, kuna need on rohkem seotud katvuse mÔÔtmise tööriistaga kui CMake'iga. Oluline on mĂ€rkida, et tulemuste pĂ”hjal luuakse eesmĂ€rk coverage, millega on mugav katvuse mÔÔtmist kĂ€ivitada.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE)
    message(STATUS "Koodikattuvuse mÔÔtmine on sisse lĂŒlitatud")

    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 "Koodikattuvuse mÔÔtmiseks on vajalik programm gcovr")
endif()

Dokumentatsiooni skript (doc/CMakeLists.txt)

Leitud Doxygen.

find_package(Doxygen)

SeejĂ€rel kontrollime, kas kasutaja on mÀÀranud keele muutujat. Kui jah, siis ei muuda midagi; kui ei, siis valime vene. SeejĂ€rel seadistame Doxygen sĂŒsteemi failid. KĂ”ik vajalikud muutujad, sealhulgas keel, sisestatakse sinna seadistamise protsessi kĂ€igus (vt. configure_file kĂ€sku).

PĂ€rast seda loome sihte dok, mis kĂ€ivitab dokumentatsiooni genereerimise. Kuna dokumentatsiooni genereerimine ei ole arenduse kĂ€igus kĂ”ige hĂ€davajalikum, siis on eesmĂ€rk vaikesĂ€tte jĂ€rgi vĂ€lja lĂŒlitatud ja tuleb selgelt kĂ€ivitada.

if (Doxygen_FOUND)
    if (NOT MYLIB_DOXYGEN_LANGUAGE)
        set(MYLIB_DOXYGEN_LANGUAGE Estonian)
    endif()
    message(STATUS "Doxygen dokumentatsioon genereeritakse keeles ${MYLIB_DOXYGEN_LANGUAGE}")
    configure_file(Doxyfile.in Doxyfile)
    add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

Veebihoiuruumi skript (online/CMakeLists.txt)

Siin leiame kolmanda Pythoni ja loome eesmÀrgi wandbox, mis genereerib pÀringu, mis vastab teenuse API-le Wandbox, ja saadab selle Àra. Vastuseks tuleb link valmis liivakasti.

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 "Veebipoli liivakasti loomiseks on vajalik 3. versiooni Python tÔlgendaja")
endif()

Projekt vÀljastpoolt

NĂŒĂŒd vaatame, kuidas kĂ”ike seda kasutada.

Koostamine

Selle projekti koostamine, nagu iga teise CMake'i koostamissĂŒsteemis, koosneb kahest etapist:

Generatsioon

cmake -S tee/kallid -B tee/kallid/koostamisdirektorid [valikud ...]

Kui eespool olev kÀsk ei töötanud vanema CMake'i versiooni tÔttu, proovige kirjutada -S:

cmake tee/kallid -B tee/kallid/koostamisdirektorid [valikud ...]

Lisainfot valikute kohta.

Projekti koostamine

cmake --build tee/k/ehitamis/kaust

Rohkem teavet ehitussihtide kohta.

Valikud

MYLIB_KATTE

cmake -S ... -B ... -DMYLIB_KATTESUS=ON [teised valikud ...]

Sisaldab sihti coverage, mille abil saab teha koodikattete mÔÔtmisi testide kaudu.

MYLIB_TESTIMINE

cmake -S ... -B ... -DMYLIB_TESTIMINE=OFF [teised valikud ...]

Pakub vĂ”imalust vĂ€lja lĂŒlitada moodulitestide ehitus ja siht kontrolli. SeetĂ”ttu lĂŒlitatakse vĂ€lja koodikattete mÔÔtmine (vt MYLIB_KATTE).

Testimine lĂŒlitatakse automaatselt vĂ€lja ka siis, kui projekt lisatakse teise projekti alaprojektina kĂ€suga add_subdirectory.

MYLIB_DOXYGEN_VEEBIMISKEEL

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [teised valikud ...]

LĂŒlitab dokumentatsiooni keele, mida siht genereerib dok mÀÀratletud keelele. Saadaval olevate keelte loetelu leiate Doxygen sĂŒsteemi veebisaidilt.

Vaikimisi on lubatud vene keel.

Kogumis eesmÀrgid

Vaikimisi

cmake --build tee/k/ehitamis/kaust
cmake --build tee/k/ehitamis/kaust --target all

Kui siht pole mÀÀratud (mis on ekvivalentne sihiga all), ehitab kÔik saadaval olevad ja kutsub esile sihi kontrolli.

mylib-unit-tests

cmake --build tee/k/ehitamis/kaust --target mylib-unit-tests

Kompileerib moodulitestid. Vaikimisi lubatud.

kontrolli

cmake --build tee/k/ehitamis/kaust --target check

KĂ€ivitab koostatud (ehitab, kui veel mitte) moodulitestid. Vaikimisi lubatud.

Vaata ka mylib-unit-tests.

coverage

cmake --build tee/k/kokkglib/direktori --target coverage

AnalĂŒĂŒsib kĂ€ivitatud (kĂ€ivitab, kui veel mitte) mooduliteste testika katte osas gcovr.

KattekÀive nÀeb vÀlja umbes nii:

------------------------------------------------------------------------------
                           GCC Koodi Katte Aruanne
Kataloog: /path/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
Fail                                        Read    TĂ€itmine  Katte  Puudu
------------------------------------------------------------------------------
mylib/myfeature.hpp                            2       2   100%   
------------------------------------------------------------------------------
KOKKU                                         2       2   100%
------------------------------------------------------------------------------

SihtmÀrk on saadaval ainult lubatud valiku korral MYLIB_KATTE.

Vaata ka kontrolli.

dok

cmake --build tee/k/kokkglib/direktori --target doc

KĂ€ivitab koodi dokumentatsiooni genereerimise sĂŒsteemi kaudu Doxygen.

wandbox

cmake --build tee/k/kokkglib/direktori --target wandbox

Teenuse vastus nÀeb vÀlja umbes nii:

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

Selleks kasutatakse teenust Wandbox. Ei tea, kui vastupidavad nende serverid on, kuid arvan, et selle vÔimaluse kuritarvitamine pole mÔistlik.

NĂ€ited

Projekt tuleb ĂŒles ehitada silumisreĆŸiimis katte mÔÔtmisega

cmake -S tee/ka/allikad -B tee/ka/ehitus/kaust -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build tee/ka/ehitus/kaust --target coverage --parallel 16

Projekti installimine ilma eelneva ehitamise ja testimiseta

cmake -S tee/ka/allikad -B tee/ka/ehitus/kaust -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=tee/ka/installimise/kaust
cmake --build tee/ka/ehitus/kaust --target install

Ehitus vabastamise reĆŸiimis mÀÀratud kompilaatoriga

cmake -S tee/ka/allikad -B tee/ka/ehitus/kaust -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=tee/ka/kausta/kuhu/on/paigaldatud/sÔltuvused
cmake --build tee/ka/ehitus/kaust --parallel 4

Dokumentatsiooni genereerimine inglise keeles

cmake -S tee/ka/allikad -B tee/ka/ehitus/kaust -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build tee/ka/ehitus/kaust --target doc

Tööriistad

  1. CMake 3.13

    Tegelikult on CMake 3.13 versioon vajalik ainult mĂ”nede kĂ€surea kĂ€skude kĂ€ivitamiseks, mis on selles juhendis kirjeldatud. CMake-skriptide sĂŒntaksist piisab versiooni 3.8 kasutamisest, kui genereerimist kutsutakse muude viisidega.

  2. Testimise raamatukogu doctest

    Testimise saab vĂ€lja lĂŒlitada (vt valik MYLIB_TESTING).

  3. Doxygen

    Keele, millel dokumentatsioon genereeritakse, saab valida MYLIB_DOXYGEN_VEEBIMISKEEL.

  4. Programmeerimiskeelte tÔlgendus Python 3

    Automaatseks genereerimiseks veebihĂŒljest.

Statiline analĂŒĂŒs

CMake’i ja paar hea tööriista abil saab staatilise analĂŒĂŒsi teha minimaalse vaevaga.

Cppcheck

CMake’is on sisseehitatud tugi staatilise analĂŒĂŒsi tööriistale Cppcheck.

Selleks tuleb kasutada valikut CMAKE_CXX_CPPCHECK:

cmake -S tee/koodiallikatele -B tee/ehitustehnikate -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Itee/koodiallikate/include"

PĂ€rast seda kĂ€ivitub staatiline analĂŒĂŒs automaatselt iga kord, kui kompileeritakse ja uuesti kompileeritakse lĂ€htekoodi. Ükski lisategevus pole vajalik.

Clang

Kasutades imelist tööriista scan-build saab samuti staatilist analĂŒĂŒsi kiirelt kĂ€ivitada:

scan-build cmake -S tee/koodiallikatele -B tee/ehitustehnikate -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build tee/ehitustehnikate

Siin, erinevalt Cppcheckist, on iga kord vajalik kÀivitada ehitus lÀbi scan-build.

JÀrelsÔna

CMake on vĂ€ga vĂ”imas ja paindlik sĂŒsteem, mis vĂ”imaldab realiseerida funktsionaalsust igasuguste soovide ja vajaduste jĂ€rgi. Ja kuigi sĂŒntaks jĂ€tab mĂ”nikord soovida, ei ole asi tegelikult nii hull, kui teda maalitakse. Kasutage CMake’i koostesĂŒsteemi ĂŒhiskonna hĂŒvanguks ja oma tervise nimel.

→ Laadi alla projekti mall

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster