Kuinka määrittää PVS-Studio Travis CI:ssä käyttämällä esimerkkiä PSP-pelikonsoliemulaattorista

Kuinka määrittää PVS-Studio Travis CI:ssä käyttämällä esimerkkiä PSP-pelikonsoliemulaattorista
Travis CI on hajautettu verkkopalvelu ohjelmistojen rakentamiseen ja testaamiseen, joka käyttää GitHubia lähdekoodin isännöinnissä. Yllä olevien käyttöskenaarioiden lisäksi voit lisätä omiasi laajojen konfigurointivaihtoehtojen ansiosta. Tässä artikkelissa määritämme Travis CI:n toimimaan PVS-Studion kanssa käyttämällä PPSSPP-koodiesimerkkiä.

Esittely

Travis CI on verkkopalvelu ohjelmistojen rakentamiseen ja testaamiseen. Sitä käytetään yleensä jatkuvan integroinnin yhteydessä.

PPSSPP - PSP-pelikonsolin emulaattori. Ohjelma pystyy jäljittelemään minkä tahansa pelin käynnistämistä Sony PSP:lle tarkoitetuista levykuvista. Ohjelma julkaistiin 1. PPSSPP on lisensoitu GPL v2012:lla. Kuka tahansa voi tehdä parannuksia projektin lähdekoodi.

PVS-studio — staattinen koodianalysaattori virheiden ja mahdollisten haavoittuvuuksien etsimiseen ohjelmakoodista. Tässä artikkelissa käynnistämme vaihteeksi PVS-Studion paikallisesti kehittäjän koneella, vaan pilvessä ja etsimme virheitä PPSSPP:stä.

Travis CI:n käyttöönotto

Tarvitsemme GitHubissa arkiston, jossa tarvitsemamme projekti sijaitsee, sekä avaimen PVS-Studiolle (voit saada kokeiluavain tai ilmainen avoimen lähdekoodin projekteille).

Mennään sivustolle Travis CI. Kun valtuutus on tehty GitHub-tililläsi, näemme luettelon arkistoista:

Kuinka määrittää PVS-Studio Travis CI:ssä käyttämällä esimerkkiä PSP-pelikonsoliemulaattorista
Testiä varten haaroitin PPSSPP:n.

Aktivoimme arkiston, jonka haluamme kerätä:

Kuinka määrittää PVS-Studio Travis CI:ssä käyttämällä esimerkkiä PSP-pelikonsoliemulaattorista
Tällä hetkellä Travis CI ei voi rakentaa projektiamme, koska rakentamiseen ei ole ohjeita. On siis asetusten aika.

Analyysin aikana jotkut muuttujat ovat hyödyllisiä meille, esimerkiksi PVS-Studion avain, jota ei ole toivottavaa määrittää asetustiedostossa. Lisätään siis ympäristömuuttujat Travis CI:n koontiasetusten avulla:

Kuinka määrittää PVS-Studio Travis CI:ssä käyttämällä esimerkkiä PSP-pelikonsoliemulaattorista
Tarvitaan:

  • PVS_USERNAME - käyttäjätunnus
  • PVS_KEY - avain
  • MAIL_USER - sähköposti, jota käytetään raportin lähettämiseen
  • MAIL_PASSWORD - sähköpostin salasana

Kaksi viimeistä ovat valinnaisia. Näitä käytetään tulosten lähettämiseen postitse. Jos haluat jakaa raportin muulla tavalla, sinun ei tarvitse ilmoittaa niitä.

Joten olemme lisänneet tarvitsemamme ympäristömuuttujat:

Kuinka määrittää PVS-Studio Travis CI:ssä käyttämällä esimerkkiä PSP-pelikonsoliemulaattorista
Luodaan nyt tiedosto .travis.yml ja aseta se projektin ytimeen. PPSSPP:llä oli jo konfiguraatiotiedosto Travis CI:lle, mutta se oli liian suuri ja täysin sopimaton esimerkkiin, joten jouduimme yksinkertaistamaan sitä suuresti ja jättämään vain peruselementit.

Ilmoitetaan ensin kieli, Ubuntu Linuxin versio, jota haluamme käyttää virtuaalikoneessa, ja tarvittavat paketit rakentamiseen:

language: cpp
dist: xenial

addons:
  apt:
    update: true
    packages:
      - ant
      - aria2
      - build-essential
      - cmake
      - libgl1-mesa-dev
      - libglu1-mesa-dev
      - libsdl2-dev
      - pv
      - sendemail
      - software-properties-common
    sources:
      - sourceline: 'ppa:ubuntu-toolchain-r/test'
      - sourceline: 'ppa:ubuntu-sdk-team/ppa'

Kaikki listatut paketit tarvitaan yksinomaan PPSSPP:tä varten.

Nyt osoitamme kokoonpanomatriisin:

matrix:
  include:
    - os: linux
      compiler: "gcc"
      env: PPSSPP_BUILD_TYPE=Linux PVS_ANALYZE=Yes
    - os: linux
      compiler: "clang"
      env: PPSSPP_BUILD_TYPE=Linux

Hieman lisää jaksosta matriisi. Travis CI:ssä on kaksi tapaa luoda koontiasetuksia: ensimmäinen on määrittää luettelo kääntäjistä, käyttöjärjestelmätyypeistä, ympäristömuuttujista jne., minkä jälkeen luodaan matriisi kaikista mahdollisista yhdistelmistä; toinen on selkeä osoitus matriisista. Voit tietysti yhdistää nämä kaksi lähestymistapaa ja lisätä ainutlaatuisen tapauksen tai päinvastoin sulkea sen pois osion avulla sulkea. Voit lukea tästä lisää kohdasta Travis CI:n dokumentaatio.

Jäljelle jää vain projektikohtaiset asennusohjeet:

before_install:
  - travis_retry bash .travis.sh travis_before_install

install:
  - travis_retry bash .travis.sh travis_install

script:
  - bash .travis.sh travis_script

after_success:
  - bash .travis.sh travis_after_success

Travis CI:n avulla voit lisätä omia komentojasi virtuaalikoneen eri elämänvaiheisiin. osio ennen_asennusta suoritetaan ennen pakettien asentamista. Sitten asentaa, joka seuraa pakettien asennusta luettelosta addons.aptjonka ilmoitimme edellä. Itse kokoonpano tapahtuu sisään käsikirjoitus. Jos kaikki meni hyvin, löydämme itsemme jälkeen_menestyksen (Tässä osiossa suoritamme staattisen analyysin). Nämä eivät ole kaikkia vaiheita, joita voidaan muokata, jos tarvitset lisää, sinun kannattaa katsoa sisään Travis CI:n dokumentaatio.

Lukemisen helpottamiseksi komennot sijoitettiin erilliseen komentosarjaan .travis.sh, joka sijoitetaan projektin juureen.

Meillä on siis seuraava tiedosto .travis.yml:

language: cpp
dist: xenial

addons:
  apt:
    update: true
    packages:
      - ant
      - aria2
      - build-essential
      - cmake
      - libgl1-mesa-dev
      - libglu1-mesa-dev
      - libsdl2-dev
      - pv
      - sendemail
      - software-properties-common
    sources:
      - sourceline: 'ppa:ubuntu-toolchain-r/test'
      - sourceline: 'ppa:ubuntu-sdk-team/ppa'

matrix:
  include:
    - os: linux
      compiler: "gcc"
      env: PVS_ANALYZE=Yes
    - os: linux
      compiler: "clang"

before_install:
  - travis_retry bash .travis.sh travis_before_install

install:
  - travis_retry bash .travis.sh travis_install

script:
  - bash .travis.sh travis_script

after_success:
  - bash .travis.sh travis_after_success

Ennen pakettien asentamista päivitämme alimoduulit. Tätä tarvitaan PPSSPP:n rakentamiseen. Lisätään ensimmäinen funktio .travis.sh (huomaa laajennus):

travis_before_install() {
  git submodule update --init --recursive
}

Nyt siirrymme suoraan PVS-Studion automaattisen käynnistyksen asettamiseen Travis CI:ssä. Ensin meidän on asennettava PVS-Studio-paketti järjestelmään:

travis_install() {
  if [ "$CXX" = "g++" ]; then
    sudo apt-get install -qq g++-4.8
  fi
  
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    wget -q -O - https://files.viva64.com/etc/pubkey.txt 
      | sudo apt-key add -
    sudo wget -O /etc/apt/sources.list.d/viva64.list 
      https://files.viva64.com/etc/viva64.list  
    
    sudo apt-get update -qq
    sudo apt-get install -qq pvs-studio 
                             libio-socket-ssl-perl 
                             libnet-ssleay-perl
  fi
    
  download_extract 
    "https://cmake.org/files/v3.6/cmake-3.6.2-Linux-x86_64.tar.gz" 
    cmake-3.6.2-Linux-x86_64.tar.gz
}

Toiminnon alussa travis_install asennamme tarvitsemamme kääntäjät käyttämällä ympäristömuuttujia. Sitten jos muuttuja $PVS_ANALYZE tallentaa arvoa Kyllä (ilmoitimme sen kohdassa env rakennusmatriisin määrityksen aikana), asennamme paketin pvs-studio. Tämän lisäksi ilmoitetaan myös paketit libio-socket-ssl-perl и libnet-ssleay-perlNe ovat kuitenkin pakollisia tulosten postittamiseen, joten ne eivät ole välttämättömiä, jos olet valinnut toisen tavan toimittaa raporttisi.

Toiminto lataus_ote lataa ja purkaa määritetyn arkiston:

download_extract() {
  aria2c -x 16 $1 -o $2
  tar -xf $2
}

On aika koota projekti. Tämä tapahtuu osiossa käsikirjoitus:

travis_script() {
  if [ -d cmake-3.6.2-Linux-x86_64 ]; then
    export PATH=$(pwd)/cmake-3.6.2-Linux-x86_64/bin:$PATH
  fi
  
  CMAKE_ARGS="-DHEADLESS=ON ${CMAKE_ARGS}"
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
  fi
  cmake $CMAKE_ARGS CMakeLists.txt
  make
}

Itse asiassa tämä on yksinkertaistettu alkuperäinen kokoonpano, lukuun ottamatta näitä rivejä:

if [ "$PVS_ANALYZE" = "Yes" ]; then
  CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
fi

Tässä koodin osassa määritimme cmake lippu käännöskomentojen vientiä varten. Tämä on välttämätöntä staattisen koodin analysaattorille. Voit lukea tästä lisää artikkelista "Kuinka käyttää PVS-Studioa Linuxissa ja macOS:ssä".

Jos kokoonpano onnistui, niin päästään jälkeen_menestyksen, jossa suoritamme staattisen analyysin:

travis_after_success() {
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
    pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                    -o PVS-Studio-${CC}.log 
                                    --disableLicenseExpirationCheck
    
    plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html
    sendemail -t [email protected] 
              -u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -s smtp.gmail.com:587 
              -xu $MAIL_USER 
              -xp $MAIL_PASSWORD 
              -o tls=yes 
              -f $MAIL_USER 
              -a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
  fi
}

Tarkastellaanpa tarkemmin seuraavia rivejä:

pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                -o PVS-Studio-${CC}.log 
                                --disableLicenseExpirationCheck
plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html

Ensimmäinen rivi luo lisenssitiedoston käyttäjänimestä ja avaimesta, jotka määritimme heti Travis CI -ympäristömuuttujia määritettäessä.

Toinen rivi aloittaa analyysin suoraan. Lippu -j määrittää analyysin säikeiden lukumäärän, lippu -l osoittaa lisenssiä, lippua -o määrittää tiedoston lokien tulostamista varten ja lipun -DisableLicenseExpirationCheck vaaditaan kokeiluversioille, koska oletuksena pvs-studio-analysaattori varoittaa käyttäjää, että käyttöoikeus on vanhentumassa. Voit estää tämän tapahtuman määrittämällä tämän lipun.

Lokitiedosto sisältää raakatulosteen, jota ei voida lukea ilman muuntamista, joten sinun on ensin tehtävä tiedosto luettavaksi. Viedään lokit läpi plog-muunnin, ja tulos on html-tiedosto.

Tässä esimerkissä päätin lähettää raportit postitse komennolla lähettää sähköpostia.

Tuloksena saimme seuraavan tiedoston .travis.sh:

#/bin/bash

travis_before_install() {
  git submodule update --init --recursive
}

download_extract() {
  aria2c -x 16 $1 -o $2
  tar -xf $2
}

travis_install() {
  if [ "$CXX" = "g++" ]; then
    sudo apt-get install -qq g++-4.8
  fi
  
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    wget -q -O - https://files.viva64.com/etc/pubkey.txt 
      | sudo apt-key add -
    sudo wget -O /etc/apt/sources.list.d/viva64.list 
      https://files.viva64.com/etc/viva64.list  
    
    sudo apt-get update -qq
    sudo apt-get install -qq pvs-studio 
                             libio-socket-ssl-perl 
                             libnet-ssleay-perl
  fi
    
  download_extract 
    "https://cmake.org/files/v3.6/cmake-3.6.2-Linux-x86_64.tar.gz" 
    cmake-3.6.2-Linux-x86_64.tar.gz
}
travis_script() {
  if [ -d cmake-3.6.2-Linux-x86_64 ]; then
    export PATH=$(pwd)/cmake-3.6.2-Linux-x86_64/bin:$PATH
  fi
  
  CMAKE_ARGS="-DHEADLESS=ON ${CMAKE_ARGS}"
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
  fi
  cmake $CMAKE_ARGS CMakeLists.txt
  make
}
travis_after_success() {
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
    pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                    -o PVS-Studio-${CC}.log 
                                    --disableLicenseExpirationCheck
    
    plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html
    sendemail -t [email protected] 
              -u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -s smtp.gmail.com:587 
              -xu $MAIL_USER 
              -xp $MAIL_PASSWORD 
              -o tls=yes 
              -f $MAIL_USER 
              -a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
  fi
}
set -e
set -x

$1;

Nyt on aika työntää muutokset git-tietovarastoon, minkä jälkeen Travis CI suorittaa koontiversion automaattisesti. Napsauta "ppsspp" päästäksesi koontiraportteihin:

Kuinka määrittää PVS-Studio Travis CI:ssä käyttämällä esimerkkiä PSP-pelikonsoliemulaattorista
Näemme yleiskatsauksen nykyisestä rakenteesta:

Kuinka määrittää PVS-Studio Travis CI:ssä käyttämällä esimerkkiä PSP-pelikonsoliemulaattorista
Jos rakentaminen onnistuu, saamme sähköpostin staattisen analyysin tuloksista. Postitus ei tietenkään ole ainoa tapa saada raportti. Voit valita minkä tahansa toteutustavan. Mutta on tärkeää muistaa, että kun rakennus on valmis, virtuaalikoneen tiedostoihin ei ole mahdollista päästä käsiksi.

Virheyhteenveto

Olemme onnistuneesti suorittaneet vaikeimman osan. Varmistetaan nyt, että kaikki ponnistelumme ovat sen arvoisia. Katsotaanpa joitain mielenkiintoisia kohtia minulle postitse saapuneesta staattisen analyysin raportista (en turhaan ilmoittanut sitä).

Vaarallinen optimointi

void sha1( unsigned char *input, int ilen, unsigned char output[20] )
{
  sha1_context ctx;

  sha1_starts( &ctx );
  sha1_update( &ctx, input, ilen );
  sha1_finish( &ctx, output );

  memset( &ctx, 0, sizeof( sha1_context ) );
}

PVS-Studion varoitus: V597 Kääntäjä voi poistaa "memset"-funktiokutsun, jota käytetään "summa"-puskurin tyhjentämiseen. RtlSecureZeroMemory()-toimintoa tulee käyttää yksityisten tietojen poistamiseen. sha1.cpp 325

Tämä koodinpätkä sijaitsee suojatussa hajautusmoduulissa, mutta se sisältää vakavan tietoturvavirheen (CWE-14). Katsotaanpa kokoonpanoluetteloa, joka luodaan Debug-versiota käännettäessä:

; Line 355
  mov r8d, 20
  xor edx, edx
  lea rcx, QWORD PTR sum$[rsp]
  call memset
; Line 356

Kaikki on kunnossa ja toimii memeset suoritetaan, mikä korvaa tärkeät tiedot RAM-muistissa, mutta älä iloitse vielä. Katsotaanpa Release-version kokoonpanoluetteloa optimoinnin kanssa:

; 354  :
; 355  :  memset( sum, 0, sizeof( sum ) );
; 356  :}

Kuten listauksesta voidaan nähdä, kääntäjä jätti kutsun huomioimatta memeset. Tämä johtuu siitä, että funktiossa sha1 puhelun jälkeen memeset ei enää viitata rakenteeseen ctx. Siksi kääntäjä ei näe mitään järkeä tuhlata prosessorin aikaa korvaamaan muistia, jota ei käytetä tulevaisuudessa. Voit korjata tämän käyttämällä toimintoa RtlSecureZeroMemory tai samanlainen hänelle.

korjaa:

void sha1( unsigned char *input, int ilen, unsigned char output[20] )
{
  sha1_context ctx;

  sha1_starts( &ctx );
  sha1_update( &ctx, input, ilen );
  sha1_finish( &ctx, output );

  RtlSecureZeroMemory(&ctx, sizeof( sha1_context ) );
} 

Turha vertailu

static u32 sceAudioOutputPannedBlocking
             (u32 chan, int leftvol, int rightvol, u32 samplePtr) {
  int result = 0;
  // For some reason, this is the only one that checks for negative.
  if (leftvol > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || rightvol < 0) {
    ....
  } else {
    if (leftvol >= 0) {
      chans[chan].leftVolume = leftvol;
    }
    if (rightvol >= 0) {
      chans[chan].rightVolume = rightvol;
    }
    chans[chan].sampleAddress = samplePtr;
    result = __AudioEnqueue(chans[chan], chan, true);
  }
}

PVS-Studion varoitus: V547 Lauseke 'leftvol >= 0' on aina tosi. sceAudio.cpp 120

Kiinnitä ensin huomiota muuhun haaraan if. Koodi suoritetaan vain, jos kaikki ehdot täyttyvät vasen vol > 0xFFFF || rightvol > 0xFFFF || vasen vol < 0 || oikea tilavuus < 0 tulee valheeksi. Siksi saamme seuraavat väitteet, jotka pätevät else-haaraan: leftvol <= 0xFFFF, rightvol <= 0xFFFF, vasen vol >= 0 и oikea tila >= 0. Huomaa kaksi viimeistä väitettä. Onko järkevää tarkistaa, mikä on välttämätön ehto tämän koodinpalan suorittamiselle?

Joten voimme turvallisesti poistaa nämä ehdolliset lauseet:

static u32 sceAudioOutputPannedBlocking
(u32 chan, int leftvol, int rightvol, u32 samplePtr) {
  int result = 0;
  // For some reason, this is the only one that checks for negative.
  if (leftvol > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || rightvol < 0) {
    ....
  } else {
    chans[chan].leftVolume = leftvol;
    chans[chan].rightVolume = rightvol;

    chans[chan].sampleAddress = samplePtr;
    result = __AudioEnqueue(chans[chan], chan, true);
  }
}

Toinen skenaario. Näiden ylimääräisten olosuhteiden takana on jonkinlainen virhe. Ehkä he eivät tarkistaneet, mitä vaadittiin.

Ctrl+C Ctrl+V iskee takaisin

static u32 scePsmfSetPsmf(u32 psmfStruct, u32 psmfData) {
  if (!Memory::IsValidAddress(psmfData) ||
      !Memory::IsValidAddress(psmfData)) {
    return hleReportError(ME, SCE_KERNEL_ERROR_ILLEGAL_ADDRESS, "bad address");
  }
  ....
}

V501 Kohteen '||' vasemmalla ja oikealla puolella on identtiset alilausekkeet '!Memory::IsValidAddress(psmfData)'. operaattori. scePsmf.cpp 703

Kiinnitä huomiota sisällä olevaan sekkiin if. Eikö ole outoa, että tarkistamme, onko osoite oikea? psmfData, kaksi kertaa niin paljon? Joten tämä tuntuu minusta oudolta... Itse asiassa tämä on tietysti kirjoitusvirhe, ja idea oli tarkistaa molemmat syöttöparametrit.

Oikea vaihtoehto:

static u32 scePsmfSetPsmf(u32 psmfStruct, u32 psmfData) {
  if (!Memory::IsValidAddress(psmfStruct) ||
      !Memory::IsValidAddress(psmfData)) {
    return hleReportError(ME, SCE_KERNEL_ERROR_ILLEGAL_ADDRESS, "bad address");
  }
  ....
}

Unohtunut muuttuja

extern void ud_translate_att(
  int size = 0;
  ....
  if (size == 8) {
    ud_asmprintf(u, "b");
  } else if (size == 16) {
    ud_asmprintf(u, "w");
  } else if (size == 64) {
    ud_asmprintf(u, "q");
  }
  ....
}

PVS-Studion varoitus: V547 Lauseke "koko == 8" on aina epätosi. syn-att.c 195

Tämä virhe sijaitsee kansiossa ext, joten se ei varsinaisesti liity projektiin, mutta vika löydettiin ennen kuin huomasin sen, joten päätin jättää sen. Loppujen lopuksi tämä artikkeli ei käsittele virheiden tarkistamista, vaan integraatiota Travis CI:n kanssa, eikä analysaattorin konfigurointia suoritettu.

muuttuja koko alustetaan vakiolla, mutta sitä ei käytetä koodissa ollenkaan operaattoriin asti if, joka tietysti antaa väärä kun tarkastelimme ehtoja, koska kuten muistamme, koko yhtä suuri kuin nolla. Myöskään myöhemmissä tarkastuksissa ei ole mitään järkeä.

Ilmeisesti koodinpätkän kirjoittaja unohti ylikirjoittaa muuttujan koko sitä ennen.

stop

Tähän todennäköisesti päädymme virheisiin. Tämän artikkelin tarkoituksena on esitellä PVS-Studion työtä yhdessä Travis CI:n kanssa, eikä analysoida projektia mahdollisimman perusteellisesti. Jos haluat suurempia ja kauniimpia virheitä, voit aina ihailla niitä täällä :).

Johtopäätös

Käyttämällä verkkopalveluita projektien rakentamiseen yhdessä inkrementaalisen analyysin kanssa voit löytää monia ongelmia heti koodin yhdistämisen jälkeen. Yksi koontiversio ei kuitenkaan välttämättä riitä, joten testauksen asettaminen yhdessä staattisen analyysin kanssa parantaa koodin laatua merkittävästi.

Hyödyllisiä linkkejä

Kuinka määrittää PVS-Studio Travis CI:ssä käyttämällä esimerkkiä PSP-pelikonsoliemulaattorista

Jos haluat jakaa tämän artikkelin englanninkielisen yleisön kanssa, käytä käännöslinkkiä: Maxim Zvyagintsev. Kuinka ottaa PVS-Studio käyttöön Travis CI:ssä PSP-pelikonsoliemulaattorin esimerkin avulla.

Lähde: will.com

Lisää kommentti