Bagaimana untuk mengkonfigurasi PVS-Studio dalam Travis CI menggunakan contoh emulator konsol permainan PSP

Bagaimana untuk mengkonfigurasi PVS-Studio dalam Travis CI menggunakan contoh emulator konsol permainan PSP
Travis CI ialah perkhidmatan web teragih untuk membina dan menguji perisian yang menggunakan GitHub sebagai pengehosan kod sumber. Sebagai tambahan kepada senario operasi di atas, anda boleh menambah terima kasih anda sendiri kepada pilihan konfigurasi yang luas. Dalam artikel ini kami akan mengkonfigurasi Travis CI untuk bekerja dengan PVS-Studio menggunakan contoh kod PPSSPP.

Pengenalan

Travis CI ialah perkhidmatan web untuk membina dan menguji perisian. Ia biasanya digunakan bersama dengan amalan penyepaduan berterusan.

PPSSPP β€” Emulator konsol permainan PSP. Program ini dapat mencontohi pelancaran mana-mana permainan daripada imej cakera yang dimaksudkan untuk Sony PSP. Program ini telah dikeluarkan pada 1 November 2012. PPSSPP dilesenkan di bawah GPL v2. Sesiapa sahaja boleh membuat penambahbaikan kepada kod sumber projek.

PVS-Studio β€” penganalisis kod statik untuk mencari ralat dan potensi kelemahan dalam kod program. Dalam artikel ini, untuk perubahan, kami akan melancarkan PVS-Studio bukan secara tempatan pada mesin pembangun, tetapi dalam awan, dan mencari ralat dalam PPSSPP.

Menyediakan Travis CI

Kami memerlukan repositori di GitHub, di mana projek yang kami perlukan terletak, serta kunci untuk PVS-Studio (anda boleh mendapatkan kunci percubaan atau percuma untuk projek Sumber Terbuka).

Mari pergi ke tapak Travis CI. Selepas kebenaran menggunakan akaun GitHub anda, kami akan melihat senarai repositori:

Bagaimana untuk mengkonfigurasi PVS-Studio dalam Travis CI menggunakan contoh emulator konsol permainan PSP
Untuk ujian, saya bercabang PPSSPP.

Kami mengaktifkan repositori yang ingin kami kumpulkan:

Bagaimana untuk mengkonfigurasi PVS-Studio dalam Travis CI menggunakan contoh emulator konsol permainan PSP
Pada masa ini, Travis CI tidak dapat membina projek kami kerana tiada arahan untuk membina. Jadi sudah tiba masanya untuk konfigurasi.

Semasa analisis, beberapa pembolehubah akan berguna kepada kami, sebagai contoh, kunci untuk PVS-Studio, yang tidak diingini untuk dinyatakan dalam fail konfigurasi. Jadi mari tambahkan pembolehubah persekitaran menggunakan tetapan binaan dalam Travis CI:

Bagaimana untuk mengkonfigurasi PVS-Studio dalam Travis CI menggunakan contoh emulator konsol permainan PSP
Kita perlukan:

  • PVS_USERNAME - nama pengguna
  • PVS_KEY - kunci
  • MAIL_USER - e-mel yang akan digunakan untuk menghantar laporan
  • MAIL_PASSWORD - kata laluan e-mel

Dua yang terakhir adalah pilihan. Ini akan digunakan untuk menghantar hasil melalui mel. Jika anda ingin mengedarkan laporan dengan cara lain, anda tidak perlu menunjukkannya.

Jadi, kami telah menambah pembolehubah persekitaran yang kami perlukan:

Bagaimana untuk mengkonfigurasi PVS-Studio dalam Travis CI menggunakan contoh emulator konsol permainan PSP
Sekarang mari buat fail .travis.yml dan letakkan di akar projek. PPSSPP telah pun mempunyai fail konfigurasi untuk Travis CI, walau bagaimanapun, ia terlalu besar dan tidak sesuai untuk contoh, jadi kami perlu memudahkannya dan meninggalkan elemen asas sahaja.

Mula-mula, mari kita nyatakan bahasa, versi Ubuntu Linux yang ingin kita gunakan dalam mesin maya, dan pakej yang diperlukan untuk binaan:

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'

Semua pakej yang disenaraikan diperlukan secara eksklusif untuk PPSSPP.

Sekarang kami menunjukkan matriks pemasangan:

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

Sedikit lagi mengenai bahagian tersebut matriks. Dalam Travis CI, terdapat dua cara untuk mencipta pilihan binaan: yang pertama ialah menentukan senarai penyusun, jenis sistem pengendalian, pembolehubah persekitaran, dsb., selepas itu matriks semua kombinasi yang mungkin dijana; yang kedua ialah petunjuk eksplisit bagi matriks. Sudah tentu, anda boleh menggabungkan kedua-dua pendekatan ini dan menambah kes yang unik, atau, sebaliknya, mengecualikannya menggunakan bahagian tidak termasuk. Anda boleh membaca lebih lanjut mengenai ini dalam Dokumentasi Travis CI.

Yang tinggal hanyalah menyediakan arahan pemasangan khusus projek:

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 membolehkan anda menambah arahan anda sendiri untuk pelbagai peringkat kehidupan mesin maya. Bahagian before_install dilaksanakan sebelum memasang pakej. Kemudian memasang, yang mengikuti pemasangan pakej daripada senarai addons.aptyang kami nyatakan di atas. Perhimpunan itu sendiri berlaku di skrip. Jika semuanya berjalan lancar, maka kita akan mendapati diri kita berada di dalamnya selepas_berjaya (dalam bahagian ini kita akan menjalankan analisis statik). Ini bukan semua langkah yang boleh diubah suai, jika anda memerlukan lebih banyak, maka anda harus melihatnya Dokumentasi Travis CI.

Untuk memudahkan pembacaan, arahan diletakkan dalam skrip berasingan .travis.sh, yang diletakkan pada akar projek.

Jadi kami mempunyai fail berikut .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

Sebelum memasang pakej, kami akan mengemas kini submodul. Ini diperlukan untuk membina PPSSPP. Mari tambah fungsi pertama ke .travis.sh (perhatikan sambungan):

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

Kini kami datang terus untuk menyediakan pelancaran automatik PVS-Studio di Travis CI. Mula-mula kita perlu memasang pakej PVS-Studio pada sistem:

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
}

Pada permulaan fungsi travis_install kami memasang penyusun yang kami perlukan menggunakan pembolehubah persekitaran. Kemudian jika pembolehubah $PVS_ANALYZE menyimpan nilai Ya (kami menunjukkannya dalam bahagian env semasa konfigurasi matriks binaan), kami memasang pakej pvs-studio. Di samping itu, pakej juga ditunjukkan libio-soket-ssl-perl ΠΈ libnet-ssleay-perl, walau bagaimanapun, ia diperlukan untuk mel hasil, jadi ia tidak diperlukan jika anda telah memilih kaedah lain untuk menyampaikan laporan anda.

Fungsi muat turun_ekstrak memuat turun dan membongkar arkib yang ditentukan:

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

Sudah tiba masanya untuk menyusun projek itu. Ini berlaku dalam bahagian skrip:

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
}

Sebenarnya, ini ialah konfigurasi asal yang dipermudahkan, kecuali untuk baris ini:

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

Dalam bahagian kod ini yang kami tetapkan cicah bendera untuk mengeksport arahan kompilasi. Ini diperlukan untuk penganalisis kod statik. Anda boleh membaca lebih lanjut mengenai ini dalam artikel "Bagaimana untuk menjalankan PVS-Studio pada Linux dan macOS".

Jika perhimpunan itu berjaya, maka kita akan sampai selepas_berjaya, di mana kami melakukan analisis statik:

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
}

Mari kita lihat lebih dekat pada baris berikut:

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

Baris pertama menjana fail lesen daripada nama pengguna dan kunci yang kami nyatakan pada awal-awal lagi semasa menyediakan pembolehubah persekitaran Travis CI.

Baris kedua memulakan analisis secara langsung. Bendera -j menetapkan bilangan benang untuk analisis, bendera -l menunjukkan lesen, bendera -o mentakrifkan fail untuk mengeluarkan log, dan bendera -disableLicenseExpirationCheck diperlukan untuk versi percubaan, kerana secara lalai pvs-studio-analyzer akan memberi amaran kepada pengguna bahawa lesen akan tamat tempoh. Untuk mengelakkan perkara ini berlaku, anda boleh menentukan bendera ini.

Fail log mengandungi output mentah yang tidak boleh dibaca tanpa penukaran, jadi anda mesti membuat fail itu boleh dibaca terlebih dahulu. Mari lulus log melalui plog-converter, dan output ialah fail html.

Dalam contoh ini, saya memutuskan untuk menghantar laporan melalui mel menggunakan arahan menghantar e-mel.

Akibatnya, kami mendapat fail berikut .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;

Kini tiba masanya untuk menolak perubahan pada repositori git, selepas itu Travis CI akan menjalankan binaan secara automatik. Klik pada "ppsspp" untuk pergi ke laporan binaan:

Bagaimana untuk mengkonfigurasi PVS-Studio dalam Travis CI menggunakan contoh emulator konsol permainan PSP
Kami akan melihat gambaran keseluruhan binaan semasa:

Bagaimana untuk mengkonfigurasi PVS-Studio dalam Travis CI menggunakan contoh emulator konsol permainan PSP
Jika binaan berjaya disiapkan, kami akan menerima e-mel dengan hasil analisis statik. Sudah tentu, mel bukan satu-satunya cara untuk menerima laporan. Anda boleh memilih mana-mana kaedah pelaksanaan. Tetapi adalah penting untuk diingat bahawa selepas binaan selesai, ia tidak akan dapat mengakses fail mesin maya.

Ringkasan ralat

Kami telah berjaya menyelesaikan bahagian yang paling sukar. Sekarang mari kita pastikan bahawa semua usaha kita berbaloi. Mari lihat beberapa perkara menarik dari laporan analisis statik yang datang kepada saya melalui mel (bukan untuk apa-apa saya menunjukkannya).

Pengoptimuman berbahaya

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 ) );
}

Amaran PVS-Studio: V597 Pengkompil boleh memadamkan panggilan fungsi 'memset', yang digunakan untuk membuang penimbal 'jumlah'. Fungsi RtlSecureZeroMemory() harus digunakan untuk memadamkan data peribadi. sha1.cpp 325

Sekeping kod ini terletak dalam modul pencincangan selamat, walau bagaimanapun, ia mengandungi kecacatan keselamatan yang serius (CWE-14). Mari lihat penyenaraian pemasangan yang dijana semasa menyusun versi Nyahpepijat:

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

Semuanya teratur dan berfungsi memet dilaksanakan, dengan itu menimpa data penting dalam RAM, namun, jangan bergembira dahulu. Mari lihat penyenaraian pemasangan versi Keluaran dengan pengoptimuman:

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

Seperti yang dapat dilihat dari penyenaraian, pengkompil mengabaikan panggilan memet. Ini disebabkan oleh fakta bahawa dalam fungsi sha1 selepas panggilan memet tiada lagi rujukan kepada struktur ctx. Oleh itu, pengkompil tidak melihat gunanya membuang masa pemproses menggantikan memori yang tidak digunakan pada masa hadapan. Anda boleh membetulkannya dengan menggunakan fungsi RtlSecureZeroMemory atau serupa kepada dia.

Betul:

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 ) );
} 

Perbandingan yang tidak perlu

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);
  }
}

Amaran PVS-Studio: V547 Ungkapan 'leftvol >= 0' sentiasa benar. sceAudio.cpp 120

Beri perhatian kepada cawangan lain untuk yang pertama if. Kod akan dilaksanakan hanya jika semua syarat vol kiri > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || vol kanan < 0 akan menjadi palsu. Oleh itu, kami mendapat kenyataan berikut, yang akan menjadi benar untuk cawangan lain: vol kiri <= 0xFFFF, vol kanan <= 0xFFFF, vol kiri >= 0 ΠΈ vol kanan >= 0. Perhatikan dua kenyataan terakhir. Adakah masuk akal untuk menyemak apakah syarat yang diperlukan untuk pelaksanaan sekeping kod ini?

Jadi kami boleh mengalih keluar pernyataan bersyarat ini dengan selamat:

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);
  }
}

Satu lagi senario. Terdapat beberapa jenis ralat tersembunyi di sebalik keadaan berlebihan ini. Mungkin mereka tidak menyemak apa yang diperlukan.

Ctrl+C Ctrl+V Memukul Balik

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 Terdapat sub-ungkapan yang sama '!Memory::IsValidAddress(psmfData)' di sebelah kiri dan di sebelah kanan '||' pengendali. scePsmf.cpp 703

Perhatikan cek di dalam if. Tidakkah anda rasa pelik kita menyemak sama ada alamat itu sah? psmfData, dua kali lebih? Jadi ini kelihatan pelik kepada saya... Sebenarnya, ini, sudah tentu, kesilapan menaip, dan ideanya adalah untuk menyemak kedua-dua parameter input.

Pilihan yang betul:

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

Pembolehubah terlupa

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");
  }
  ....
}

Amaran PVS-Studio: V547 Ungkapan 'saiz == 8' sentiasa palsu. syn-att.c 195

Ralat ini terletak dalam folder ext, jadi tidak begitu relevan dengan projek itu, tetapi pepijat telah ditemui sebelum saya menyedarinya, jadi saya memutuskan untuk meninggalkannya. Lagipun, artikel ini bukan tentang menyemak ralat, tetapi mengenai penyepaduan dengan Travis CI, dan tiada konfigurasi penganalisis dijalankan.

Pembolehubah saiz dimulakan oleh pemalar, bagaimanapun, ia tidak digunakan sama sekali dalam kod, sehingga ke operator if, yang, sudah tentu, memberi palsu sambil menyemak syarat, kerana, seperti yang kita ingat, saiz sama dengan sifar. Pemeriksaan seterusnya juga tidak masuk akal.

Nampaknya, pengarang serpihan kod terlupa untuk menulis ganti pembolehubah saiz sebelum itu.

Berhenti

Di sinilah kita mungkin akan berakhir dengan kesilapan. Tujuan artikel ini adalah untuk menunjukkan kerja PVS-Studio bersama-sama dengan Travis CI, dan bukan untuk menganalisis projek itu seteliti mungkin. Jika anda mahukan kesilapan yang lebih besar dan lebih indah, anda sentiasa boleh mengaguminya di sini :).

Kesimpulan

Menggunakan perkhidmatan web untuk membina projek bersama-sama dengan amalan analisis tambahan membolehkan anda menemui banyak masalah serta-merta selepas menggabungkan kod. Walau bagaimanapun, satu binaan mungkin tidak mencukupi, jadi menyediakan ujian bersama analisis statik akan meningkatkan kualiti kod dengan ketara.

Pautan berguna

Bagaimana untuk mengkonfigurasi PVS-Studio dalam Travis CI menggunakan contoh emulator konsol permainan PSP

Jika anda ingin berkongsi artikel ini dengan khalayak berbahasa Inggeris, sila gunakan pautan terjemahan: Maxim Zvyagintsev. Bagaimana untuk menyediakan PVS-Studio dalam Travis CI menggunakan contoh emulator konsol permainan PSP.

Sumber: www.habr.com

Tambah komen