深入理解CMake(2):初步解讀Caffe的CMake腳本

預備說明

分析的是官方Caffe(https://github.com/BVLC/caffe)的CMake腳本,主要分析了根目錄的CMakeLists.txt。
Caffe代碼的commit id為99bd99795dcdf0b1d3086a8d67ab1782a8a08383

所謂CMake腳本這里指的是CMakeLists.txt和xxx.cmake的統稱。

$CAFFE_ROOT/CMakeLists.txt解讀

cmake_minimum_required(VERSION 2.8.7)

設定cmake最低版本。高版本cmake提供更多的功能(例如cmake3.13開始提供target_link_directories())或解決bug(例如OpenMP的設定問題),低版本有更好的兼容性。VERSION必須大寫,否則不識別而報錯。非必須但常規都寫。放在最開始一行。


if(POLICY CMP0046)
  cmake_policy(SET CMP0046 NEW)
endif()

cmake中也有if判斷語句,需要配對的endif()。
POLICY是策略的意思,cmake中的poilcy用來在新版本的cmake中開啟、關閉老版本中逐漸被放棄的功能特性:

Policies in CMake are used to preserve backward compatible behavior across multiple releases


project(Caffe C CXX)

project()指令,給工程起名字,很正常不過了。這列還寫明了是C/C++工程,其實沒必要寫出來,因為CMake默認是開啟了這兩個的。
這句命令執行后,自動產生了5個變量:

  • PROJECT_NAME,值等于Caffe
  • PROJECT_SOURCE_DIR,是CMakeLists.txt所在目錄,通常是項目根目錄(奇葩的項目比如protobuf,把CMakeLists.txt放在cmake子目錄的也有)
  • PROJECT_BINARY_DIR,是執行cmake命令時所在的目錄,通常是build一類的用戶自行創建的目錄。
  • Caffe_SOURCE_DIR,此時同PROJECT_SOURCE_DIR
  • Caffe_BINARY_DIR,此時同PROJECT_BINARY_DIR
    官方cmake文檔對PROJECT_SOURCE_DIRPROJECT_BINARY_DIR的解釋很晦澀:
    image.png
image.png

自行實踐驗證下:


image.png

image.png


set(CAFFE_TARGET_VERSION "1.0.0" CACHE STRING "Caffe logical version")
set(CAFFE_TARGET_SOVERSION "1.0.0" CACHE STRING "Caffe soname version")

set()指令是設定變量的名字和取值,CACHE意思是緩存類型,是說在外部執行CMake時可以臨時指定這一變量的新取值來覆蓋cmake腳本中它的取值:CMAKE -Dvar_name=var_value

而最后面的雙引號包起來的取值可以認為是”注釋“。STRING是類型,不過據我目前看到和了解到的,CMake的變量99.9%是字符串類型,而且這個字符串類型變量和字符串數組類型毫無區分。

變量在定義的時候直接寫名字,使用它的時候則需要用${VAR_NAME}的形式。此外還可以使用系統的環境變量,形式為$ENV{ENV_VAR_NAME},例如$ENV{PATH},$ENV{HOME}等。

除了緩存變量,option()指令設定的東西也可以被用CMake -Dxxx=ON的形式來覆蓋。


add_definitions(-DCAFFE_VERSION=${CAFFE_TARGET_VERSION})

add_definitions()命令通常用來添加C/C++中的宏,例如:

  • add_defitions(-DCPY_ONLY) ,給編譯器傳遞了預定義的宏CPU_ONLY,相當于代碼中增加了一句#define CPU_ONLY
  • add_defitions(-DMAX_PATH_LEN=256),則相當于#define MAX_PATH_LEN 256
    根據文檔,實際上add_definitions()可以添加任意的編譯器flags,只不過像添加頭文件搜索路徑等flags被交給include_directory()等命令了。

在這里具體的作用是,設定CAFFE_VERSION這一C/C++宏的值為CAFFE_TARGET_VERSION變量的取值,而這一變量在前面分析過,它是緩存變量,有一個預設的默認值,也可以通過cmake .. -DCAFFE_TARGET_VERSION=x.y.z來指定為x.y.z。


list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/Modules)

這里首先是list(APPEND VAR_NAME VAR_VALUE)這一用法,表示給變量VAR_NAME追加一個元素VAR_VALUE。雖然我寫成VAR_NAME,但前面有提到,cmake中的變量幾乎都是字符串或字符串數組,這里VAR_NAME你就當它是一個數組就好了,而當后續使用${VAR_NAME}時輸出的是”整個數組的值“。(吐槽:這不就是字符串么?為什么用list這個名字呢?搞得像是在寫不純正的LIPS)

具體的說,這里是把項目根目錄(CMakeLists.txt在項目根目錄,${PROJECT_SOURCE_DIR}表示CMakeLists.txt所在目錄)下的cmake/Modules子目錄對應的路徑值,追加到CMAKE_MODULE_PATH中;CMAKE_MODULE_PATH后續可能被include()find_package()等命令所使用。


include(ExternalProject)
include(GNUInstallDirs)

include()命令的作用:

  • 包含文件,
  • 或者,包含模塊
    所謂包含文件,例如include(utils.cmake),把當前路徑下的utils.cmake包含進來,基本等同于C/C++中的#include指令。通常,include文件的話文件應該是帶有后綴名的。
    所謂包含模塊,比如include(xxx),是說在CMAKE_MODULE_PATH變量對應的目錄,或者CMake安裝包自帶的Modules目錄(比如mac下brew裝的cmake對應的是/usr/local/share/cmake/Modules)里面尋找xxx.cmake文件。注意,此時不需要寫".cmake"這一后綴。

具體的說,這里是把CMake安裝包提供的ExternalProject.cmake(例如我的是/usr/local/share/cmake/Modules/ExternalProject.cmake)文件包含進來。ExternalProject,顧名思義,引入外部工程,各種第三方庫什么的都可以考慮用它來弄;

GNUInstallDirs也是對應到CMake安裝包提供的GNUInstallDirs.cmake文件,這個包具體細節還不太了解,可自行翻閱該文件。


include(cmake/Utils.cmake)
include(cmake/Targets.cmake)
include(cmake/Misc.cmake)
include(cmake/Summary.cmake)
include(cmake/ConfigGen.cmake)

這里是實打實的包含了在項目cmake子目錄下的5各cmake腳本文件了,是Caffe作者們(注意,完整的Caffe不是Yangqing Jia一個人寫的)提供的,粗略看了下:

  • cmake/Utils.cmake: 定義了一些通用的(適用于其他項目的)函數,用于變量(數組)的打印、合并、去重、比較等(吐槽:cmake語法比較奇葩,相當一段時間之后我才發現它是lisp方式的語法,也就是函數(命令)是一等公民)
  • cmake/Targets.cmake: 定義了Caffe項目本身的一些函數和宏,例如源碼文件組織、目錄組織等。
  • cmake/Misc.cmake:雜項,比較摳細節的一些設定,比如通常CMAKE_BUILD_TYPE基本夠用了,但是這里通過CMAKE_CONFIGURATION_TYPES來輔助設定CMAKE_BUILD_TYPE,等等
  • cmake/Summary.cmake:定義了4個打印函數,用來打印Caffe的一些信息,執行CMake時會在終端輸出,相比于散落在各個地方的message()語句會更加系統一些
  • cmake/ConfigGen.cmake: 整個caffe編譯好之后,如果別的項目要用它,那它也應該用cmake腳本提供配置信息。

這5個cmake腳本中具體的函數比較多,這里先放過,后續可能考慮逐一解讀。


caffe_option(CPU_ONLY  "Build Caffe without CUDA support" OFF) # TODO: rename to USE_CUDA
caffe_option(USE_CUDNN "Build Caffe with cuDNN library support" ON IF NOT CPU_ONLY)
caffe_option(USE_NCCL "Build Caffe with NCCL library support" OFF)
caffe_option(BUILD_SHARED_LIBS "Build shared libraries" ON)
caffe_option(BUILD_python "Build Python wrapper" ON)
set(python_version "2" CACHE STRING "Specify which Python version to use")
caffe_option(BUILD_matlab "Build Matlab wrapper" OFF IF UNIX OR APPLE)
caffe_option(BUILD_docs   "Build documentation" ON IF UNIX OR APPLE)
caffe_option(BUILD_python_layer "Build the Caffe Python layer" ON)
caffe_option(USE_OPENCV "Build with OpenCV support" ON)
caffe_option(USE_LEVELDB "Build with levelDB" ON)
caffe_option(USE_LMDB "Build with lmdb" ON)
caffe_option(ALLOW_LMDB_NOLOCK "Allow MDB_NOLOCK when reading LMDB files (only if necessary)" OFF)
caffe_option(USE_OPENMP "Link with OpenMP (when your BLAS wants OpenMP and you get linker errors)" OFF)

# This code is taken from https://github.com/sh1r0/caffe-android-lib
caffe_option(USE_HDF5 "Build with hdf5" ON)

這里是設定各種option,也就是”開關“,然后后續根據開關的取值(布爾類型的變量,利用ifelse來判斷),編寫各自的構建規則。
其中caffe_option()cmake/Utils.cmake中定義的,它相比于cmake自帶的option()命令,增加了可選的條件控制字段:

image.png

caffe_option()的具體實現還沒有看懂,不過看一下所有用到的地方也都是很直觀的:

image.png

具體的說,這里就是設定一些“高層級的編譯選項開關”,比如是否編matlab接口、是否編python接口,是否用hdf5,是否用openmp,等等。


include(cmake/Dependencies.cmake)

這里是包含Dependencies.cmake,它里面配置了Caffe的絕大多數依賴庫:

Boost
Threads
OpenMP
Google-glog
Google-gflags
Google-protobuf
HDF5
LMDB
LevelDB
Snappy
CUDA
OpenCV
BLAS
Python
Matlab
Doxygen

其中每一個依賴庫庫都直接(在Dependencies.cmake中)或間接(在各自的cmake腳本文件中)使用find_package()命令來查找包。

使用find_package(),需要明確兩點:

  1. find_package(Xxx)如果執行成功,則提供相應的Xxx_INCLUDE_DIRXxx_LIBRARY_DIR等變量,看起來挺方便,但其實并不是所有的庫都提供了同樣的變量后綴,其實都是由庫的官方作者或第三方提供的xxx.cmake等腳本來得到的,依賴于生態。
  2. find_packge(Xxx)實際中往往是翻車重災區。它其實有N大查找順序,而CSDN上的博客中往往就瞎弄一個,你照搬后還是不行。具體例子:
  • 系統包管理工具裝的OpenCV不帶contrib模塊,想使用自行編譯的OpenCV但是git clone下來的開源代碼執行后找不到自己編譯的OpenCV。其實只要知道N大查找順序,設定CMAKE_PREFIX_PATH中包含OpenCV路徑后基本都能找到。
  • Caffe基于cmake編譯,依賴于Boost,系統里用apt或brew裝了Boost,同時也自行編譯了高版本Boost,現在Caffe編譯時cmake只認自行編譯版的Boost,指定N大查找順序也不能找到系統的Boost。切換已安裝的多個Boost給CMake find_package(),這時候需要看看FindBoost.cmake是怎么寫的,必須提供它里面說的字樣的變量(表示include和lib的查找路徑),才能讓find_package()起作用。
  • CMake編譯安裝了多個版本的Caffe(比如官方Caffe、SSD的Caffe),~/.cmake目錄下會緩存一個caffe,而現在手頭有一個做人臉檢測的工程依賴Caffe,而你希望它用官方Caffe而不是SSD-Caffe,這個緩存目錄很可能搗亂,這個我認為是某些項目比如Caffe的export輸出是多余的,反而容易造成混淆

這里暫時不逐一分析每一個包的find_package()情況,只需要注意如果某個包你安裝了但是cmake卻沒有找到,那就需要在find_package()前進行設定,以及之后排查。


if(UNIX OR APPLE)
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -Wall")
endif()

通過設定CMAKE_CXX_FLAGS,cmake生成各自平臺的makefile、.sln或xcodeproject文件時設定同樣的CXXFLAGS給編譯器。如果是.c文件,則由c編譯器編譯,對應的是CMAKE_C_FLAGS。

這里的set()指令設定CMAKE_CXX_FLAGS的值,加入了兩個新的flags:"-fPIC"和"-Wall"。實際上用list(APPEND CMAKE_CXX_FLAGS "-fPIC -Wall")是完全可以的。set()只不過是有時候可能考慮設定變量默認值的時候用一用。

-fPIC作用于編譯階段,告訴編譯器產生與位置無關代碼(Position-Independent Code),則產生的代碼中,沒有絕對地址,全部使用相對地址,故而代碼可以被加載器加載到內存的任意位置,都可以正確的執行。這正是共享庫所要求的,共享庫被加載時,在內存的位置不是固定的。
-Wall則是開啟所有警告。根據個人的開發經驗,C編譯器的警告不能完全忽視,有些wanring其實應當當做error來對待,例如:

  • 函數未定義而被使用(忘記#include頭文件)
  • 指針類型不兼容(incompatible)
    都有可能引發seg fault,甚至bus error。


caffe_set_caffe_link()

這里是設置Caffe_LINK這一變量,后續鏈接階段會用到。它定義在cmake/Targets.cmake中:

image.png

可以看到,如果是編共享庫(動態庫),則就叫caffe;否則,則增加一些鏈接器的flags:-Wl是告訴編譯器,后面緊跟的是鏈接器的flags而不是編譯器的flags(現在的編譯器往往是包含了調用連接器的步驟)。

這里的幾個鏈接器參數,目前我沒有細究過,具體看ld文檔:https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_3.html


if(USE_libstdcpp)
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libstdc++")
  message("-- Warning: forcing libstdc++ (controlled by USE_libstdcpp option in cmake)")
endif()

USE_libstdcpp這個變量的含義:
在前面已經include(cmake/Dependencies.cmake)的情況下,Dependencies.cmake中的include(cmake/Cuda.cmake)使得Cuda的設定也被載入。而Cuda.cmake中的最后,判斷如果當前操作系統是蘋果系統并且>10.8、cuda版本小于7.0,那么使用libstdc++而不是libc++

image.png

這時候想起來還沒畢業那會兒的一個新聞,說蘋果移除了libstdc++而讓大家換libc++的事情了,這個USE_libstdcpp就是這個意思了:如果cuda版本老(<7.0)并且OSX版本高(>10.8),就應該用libstdc++來兼容cuda。

這里還有一個小插曲:通常執行cmake后最前面會輸出它所使用的C、C++編譯器的可執行文件完整路徑,然后一個同事的機器上把CXX環境變量設為/usr/bin/gcc,導致編譯.cpp文件時是用CXX這一環境變量——也就是gcc——來編譯.cpp文件。編譯.cpp,如果是C++編譯器來編譯,鏈接階段默認會把標準庫鏈接進去,而現在是C編譯器,沒有明確指出要鏈接C++標準庫,就會導致鏈接出問題,雖然他的CMakeLists.txt中曾經加入過libstdc++庫,但是顯然這很容易翻車,CXX環境變量不應該設定為/usr/bin/gcc。


caffe_warnings_disable(CMAKE_CXX_FLAGS -Wno-sign-compare -Wno-uninitialized)

這里添加的編譯器flags,是用來屏蔽特定類型的警告的。雖說眼不見心不煩,關掉后少些warning輸出,但是0error0warning不應該是中級目標嗎?


configure_file(cmake/Templates/caffe_config.h.in "${PROJECT_BINARY_DIR}/caffe_config.h")

這是設定configure file。configure_file()命令是把輸入文件(第一個參數)里面的一些內容做替換(比如${var}@var@替換為具體的值,宏定義等),然后放到指定的輸出文件(第二個參數)。其實還有其他沒有列出的參數。

具體說,這里生成了build/caffe_config.h,里面define了幾個變量:

image.png


set(Caffe_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include)
set(Caffe_SRC_DIR ${PROJECT_SOURCE_DIR}/src)
include_directories(${PROJECT_BINARY_DIR})

這里是設定兩個自定義變量Caffe_INCLUDE_DIRCaffe_SRC_DIR的值,只不過它倆比較特殊,想想:如果以后別人find_package(Caffe),其實就需要其中的Caffe_INCLUDE_DIR的值。anyway,那些是后續export命令干的事情,這里忽略。

這里第三句include_directories()命令,把build目錄加入到頭文件搜索路徑了,其實就是為了確保caffe_config.h能被正常include(就一個地方用到它):

image.png


# cuda_compile() does not have per-call dependencies or include pathes
# (cuda_compile() has per-call flags, but we set them here too for clarity)
#
# list(REMOVE_ITEM ...) invocations remove PRIVATE and PUBLIC keywords from collected    definitions and include pathes
if(HAVE_CUDA)
  # pass include pathes to cuda_include_directories()
  set(Caffe_ALL_INCLUDE_DIRS ${Caffe_INCLUDE_DIRS})
  list(REMOVE_ITEM Caffe_ALL_INCLUDE_DIRS PRIVATE PUBLIC)
  cuda_include_directories(${Caffe_INCLUDE_DIR} ${Caffe_SRC_DIR}                         ${Caffe_ALL_INCLUDE_DIRS})

  # add definitions to nvcc flags directly
  set(Caffe_ALL_DEFINITIONS ${Caffe_DEFINITIONS})
  list(REMOVE_ITEM Caffe_ALL_DEFINITIONS PRIVATE PUBLIC)
  list(APPEND CUDA_NVCC_FLAGS ${Caffe_ALL_DEFINITIONS})
endif()

擦亮眼睛:Caffe的cmake腳本中分別定義了Caffe_INCLUDE_DIRCaffe_INCLUDE_DIRS兩個變量,只相差一個S,稍不留神容易混掉:不帶S的值是$Caffe_ROOT/include,帶S的值是各個依賴庫的頭文件搜索路徑(在Dependencies.cmake中多次list(APPEND得到的。類似的,Caffe_DEFINITIONS也是在Dependencies.cmake中設定的。

這里判斷出如果有CUDA的話就把Caffe_INCLUDE_DIRS變量中的PUBLICPRIVATE都去掉,把Caffe_DEFINITIONS中的PUBLICPRIVATE也去掉。

add_definitions()中添加的宏,用PUBLIC或PRIVATE修飾,有什么用?
以及,set()或list(APPEND來設定、更新的庫名字,用PUBLIC、PRIVATE或INTERFACE修飾,有什么用?這里比較疑惑,盡管我找到了stack overflow上的這篇回答,但是仍然一頭霧水:https://stackoverflow.com/questions/26037954/cmake-target-link-libraries-interface-dependencies

anyway,反正這里最后都做了list(REMOTE_ITEM操作,把PUBLICPRIVATE去掉了。


add_subdirectory(src/gtest)
add_subdirectory(src/caffe)
add_subdirectory(tools)
add_subdirectory(examples)
add_subdirectory(python)
add_subdirectory(matlab)
add_subdirectory(docs)

使用add_subdirectory(),意思是說把子目錄中的CMakeLists.txt文件加載過來執行,從這個角度看似乎等同于include()命令。實則不然,因為它除了按給定目錄名字后需要追加"/CMakeLists.txt"來構成完整路徑外,往往都是包含一個target(類似于git中的submodule了),同時還可以設定別的一些參數:

  • 指定binary_dir
  • 設定EXCLUDE_FROM_ALL,也就是”搞一個獨立的子工程“,此時需要有project()指令,并且不被包含在生成的.sln工程的ALL目標中,需要單獨構建。

粗略看看各個子目錄都是做什么的:

  • src/gtest,googletest的源碼
  • src/caffe,caffe的源碼構建,因為前面做了很多操作(依賴庫、路徑,etc),這里寫的就比較少。任務只有2個:構建一個叫做caffe的庫,以及test。
  • tools,這一子目錄下每一個cpp文件都生成一個xxx.bin的目標,而最常用的就是caffe訓練接口build/caffe這個可執行文件了。
  • examples,這一子目錄下有cpp_classification的C++代碼,以及mnist,cifar10,siamse這三個例子的數據轉換的代碼,這四個都是C++文件,每一個都被編譯出一個可執行
  • python,pycaffe接口,python/caffe/_caffe.cpp編譯出動態庫
  • matlab,matlab接口,./+caffe/private/caffe_.cpp編譯出?編譯出一個定制的目標,至于是啥類型,也許是動態庫吧,玩過matlab和C/C++混編的都知道,用mex編譯C/C++為.mexa文件,然后matlab調用.mexa文件,其實就是動態庫
  • docs,文檔,doxygen、jekyll都來了,以及拷貝每一個.ipynb文件。沒錯,add_custom_command()能定制各種target,只要你把想要執行的shell腳本命令用cmake的語法來寫就可以了,很強大。

add_custom_target(lint COMMAND ${CMAKE_COMMAND} -P ${PROJECT_SOURCE_DIR}/cmake/lint.cmake)

這里依然是定制的target,具體看來是調用scripts/cpplint.py(谷歌官方C++代碼風格檢查工具)來執行代碼風格檢查。(個人覺得G家的C++風格有一點不太好:縮進兩個空格太少了,費眼睛,強烈建議和Visual Studio保持一致,用tab并且tab寬度為4個空格)。

所謂linter就是語法檢查器,除了cpplint其實還可以用cpp_check、gccclang等,我的vim中配置的就是用cpp_checkgcc,不妨試試:https://github.com/zchrissirhcz/dotvim


if(BUILD_python)
  add_custom_target(pytest COMMAND python${python_version} -m unittest discover -s caffe/test WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/python )
  add_dependencies(pytest pycaffe)
endif()

如果開啟了BUILD_python開關,那么執行一個定制的target(執行pytest)。
add_dependencies()意思是指定依賴關系,這里要求pycaffe目標完成后再執行pytest目標,因為pytest需要用到pycaffe生成的caffe模塊。pycaffe在前面提到的add_subdirectory(python)中被構建。


configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Uninstall.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/cmake/Uninstall.cmake
    IMMEDIATE @ONLY)

add_custom_target(uninstall
    COMMAND ${CMAKE_COMMAND} -P
    ${CMAKE_CURRENT_BINARY_DIR}/cmake/Uninstall.cmake)

這里是添加”uninstall"這一target,具體定制的target其實就是執行cmake/Uninstall.cmake腳本。這個腳本根據cmake/Uninstall.cmake.in做變量取值替換等來生成得到。


# ---[ Configuration summary
caffe_print_configuration_summary()

# ---[ Export configs generation
caffe_generate_export_configs()

在Caffe根目錄的CMakeLists.txt的最后,是打印各種配置的總的情況,以及輸出各種配置(后者其實包含了install()指令的調用)

(2019-03-03 00:31:09 本篇寫之前覺得不難,但是斷斷續續分析下來竟然用了大半天時間,對于CMake的一些指令細節重新查過,發現之前的掌握確實還不夠)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • CMake學習 本篇分享一下有關CMake的一些學習心得以及相關使用。 本文目錄如下: [1、CMake介紹] [...
    AlphaGL閱讀 12,276評論 11 79
  • 搬運自本人 CSDN 博客:https://blog.csdn.net/ajianyingxiaoqinghan/...
    琦小蝦閱讀 15,422評論 0 11
  • 1.安裝 $sudo apt-get install cmake 2.示例:簡單的文件目錄 sample |—...
    荷包蛋醬閱讀 29,683評論 0 15
  • 注:首發地址 1. 前言 當在做 Android NDK 開發時,如果不熟悉用 CMake 來構建,讀不懂 CMa...
    cfanr閱讀 24,477評論 1 53
  • CMake 全稱“cross platform make”,是開源、跨平臺的自動化構建系統。CMake 由 Kit...
    神齊閱讀 4,144評論 0 6