CMake 命令笔记

CMake 全称“cross platform make”,是开源、跨平台的自动化构建系统。CMake 由 Kitware 开发与维护,来自使用者的贡献使得 CMake 快速成长。

CMake 并不直接建构出最终的软件,而是依照平台、编译器产生标准的建构档(如 Unix Makefile 或 Visual Studio 的 projects/workspaces),然后再依一般的生成方式使用。和标准的 GNU 开发工具相比,CMake 的角色比 Make 更高阶,比较接近 Autotools,而且支持多种不同的平台与编译器。

虽然跨平台是 CMake 的重要特色,但由于 CMake 的简单与弹性,在单一平台上使用也很便利。

前言

  • 每一个需要进行 CMake 操作的目录下面,都必须存在文件 CMakeLists.txt
  • CMake 命令不区分大小写。习惯上,CMake 命令全小写,预定义变量全大写。
  • 变量使用 ${} 方式取值,但是在 if 控制语句中是直接使用变量名。
  • command(parameter1 parameter2 …),参数使用括号括起,参数之间使用空格或分号分开。

常用预定义变量

CMake 的预定义变量

  • PROJECT_SOURCE_DIR:工程根目录;
  • PROJECT_BINARY_DIR:运行 CMake 命令的目录。建议定义为 ${PROJECT_SOURCE_DIR}/build 下;
  • CMAKE_INCLUDE_PATH:环境变量,非 CMake 变量;
  • CMAKE_LIBRARY_PATH:环境变量;
  • CMAKE_CURRENT_SOURCE_DIR:当前处理的 CMakeLists.txt 文件所在路径;
  • CMAKE_CURRENT_BINARY_DIR:target 编译目录;
    • 使用 add_subdirectory 命令可以更改该变量的值;
    • set(EXECUTABLE_OUTPUT_PATH <dir>) 命令不会对该变量有影响,但改变了最终目标文件的存储路径。
  • CMAKE_CURRENT_LIST_FILE:输出调用该变量的 CMakeLists.txt 的完整路径;
  • CMAKE_CURRENT_LIST_LINE:输出该变量所在的行;
  • CMAKE_MODULE_PATH:定义自己的 CMake 模块所在路径;
  • EXECUTABLE_OUTPUT_PATH:重新定义目标二进制可执行文件的存放位置;
  • LIBRARY_OUTPUT_PATH:重新定义目标链接库文件的存放位置;
  • PROJECT_NAME:返回由 project 命令定义的项目名称;
  • CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS:用来控制 if…else… 语句的书写方式。

系统信息预定义变量

  • CMAKE_MAJOR_VERSION:CMake 主版本号,如 3.12.0 中的 3;
  • CMAKE_MINOR_VERSION:CMake 次版本号,如 3.12.0 中的 12;
  • CMAKE_PATCH_VERSION:CMake 补丁等级,如 3.12.0 中的 0;
  • CMAKE_SYSTEM:系统名称,例如 Windows-10.0.17134;
  • CMAKE_SYSTEM_NAME:不包含版本号的系统名,如 Windows;
  • CMAKE_SYSTEM_VERSION:系统版本号,如 10.0.17134;
  • CMAKE_SYSTEM_PROCESSOR:处理器架构,如 AMD64;
  • UNIX:在所有的类 UNIX 平台为 true,包括 macOS 和 Cygwin;
  • WIN32:在所有的 Win32 平台为 true,包括 Cygwin。

开关选项

  • BUILD_SHARED_LIBS:控制默认的库编译方式;
    • 如果未进行设置,使用 add_library 时又没有指定库类型,默认编译生成的库都是静态库。
  • CMAKE_C_FLAGS:设置 C 编译选项;
  • CMAKE_CXX_FLAGS:设置 C++ 编译选项。

常用命令

cmake_minimum_required

该语句一般放置在 CMakeLists.txt 的开头,用于说明 CMake 最低版本要求。

cmake_minimum_required(VERSION 3.5)

上述示例指 CMake 的版本号最低为 3.5。

project

project(<PROJECT-NAME>)
  • <PROJECT-NAME> 指工程名称。

该命令一般紧跟 cmake_minimum_required 命令之后,定义了工程的名称。但项目最终编译生成的可执行文件并不一定是这个项目名称,而是由另一条命令确定的,后面会介绍。

执行了该命令之后,将会自动创建两个变量:

  • <PROJECT-NAME>_BINARY_DIR:二进制文件保存路径;
  • <PROJECT-NAME>_SOURCE_DIR:源代码路径。
project(Lab)

执行了上一条指令,即定义了一个项目名称 Lab,相应的会生成两个变量:Lab_BINARY_DIRLab_SOURCE_DIR

CMake 中预定义了两个变量:PROJECT_BINARY_DIRPROJECT_SOURCE_DIR

在这个例子中:

  • PROJECT_BINARY_DIR 等价于 Lab_BINARY_DIR
  • PROJECT_SOURCE_DIR 等价于 Lab_SOURCE_DIR

建议直接使用 PROJECT_BINARY_DIRPROJECT_SOURCE_DIR,这样即使项目名称发生了变化,也不会影响 CMakeLists.txt 文件。

关于 PROJECT_BINARY_DIRPROJECT_SOURCE_DIR 这两个变量是否相同的问题,涉及到编译方法是内部编译还是外部编译。如果是内部编译,则这两个变量相同;如果是外部编译,则两个变量不同。此处对内部编译与外部编译做出介绍:

外部构建与内部构建

假设此时已经完成了 CMakeLists.txt 的编写,在 CMakeLists.txt 所在目录下,有两种执行 CMake 的方法:

cmake .\\
make

以及

mkdir build
cd .\\build
cmake ..\\
make

第一种方法是内部构建,第二种方法是外部构建。上述两种方法中,最大不同在于 CMake 与 Make 的工作路径不同。

内部构建方法中,CMake 生成的中间文件和可执行文件都会存放在项目目录中;外部构建方法中,中间文件与可执行文件都存放在 build 目录中。

建议使用外部构建方法。优点显而易见:最大限度地保持了代码目录的整洁,生成、编译与安装是不同于项目目录的其他目录中,在外部构建方法下,PROJECT_SOURCE_DIR 指向目录与内部构建相同,为 CMakeLists.txt 所在根目录;而 PROJECT_BINARY_DIR 不同,它指向 CMakeLists.txt 所在根目录下的 build 目录。

set

set(<variable> <value>… CACHE <type> <docstring> [FORCE])

示例:

set(CMAKE_INSTALL_PREFIX C:\\Program Files\\${PROJECT_NAME})

该示例显式地将 CMAKE_INSTALL_PREFIX 的值定义为 C:\\Program Files\\${PROJECT_NAME}。如此,在外部构建情况下执行 make install 命令时,Make 会将生成的可执行文件拷贝到 C:\\Program Files\\${PROJECT_NAME}\\bin 目录下。

当然,可执行文件的安装路径 CMAKE_INSTALL_PREFIX 也可以在执行 cmake 命令的时候指定,cmake 参数如下:

cmake -D CMAKE_INSTALL_PREFIX="C:\\Program Files\\…"

如果 cmake 参数和 CMakeLists.txt 文件中都不指定该值的话,则该值为默认值(Windows 下为 C:\\Program Files\\${PROJECT_NAME},UNIX 下为 /usr/local)。

add_subdirectory

add_subdirectory(source_dir [binary_dir]
                 [EXCLUDE_FROM_ALL])
  • source_dir:源文件路径;
  • [binary_dir]:中间二进制与目标二进制文件存放路径;
  • [EXECLUDE_FROM_ALL]:将这个目录从编译过程中排除。

这个命令用于向当前工程添加存放源文件的子目录。

include_directories

include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 …])
  • [AFTER|BEFORE]:追加标志,指定控制追加或预先添加;
  • [SYSTEM]:指定该目录为系统包含目录;
  • dir1, …, dir n:添加的一系列头文件搜索路径。

向工程添加多个特定的头文件搜索路径,路径之间用空格分隔。类似于 GCC 中的编译参数 -l,即指定编译过程中编译器搜索头文件的路径。当项目需要的头文件不在系统默认的搜索路径时,则指定该路径。

add_executable

add_executable(<name> [WIN32] [MACOSX_BUNDLE]
               [EXCLUDE_FROM_ALL]
               [source1] [source2 …])
  • name:可执行文件名;
  • source1, …, source n:生成该可执行文件的源文件。

该命令给出源文件,并指出需要编译出的可执行文件名。

示例 1:

add_executable(hello main.cpp)

该示例中,利用源文件 main.cpp,编译出名为 hello 的可执行文件。

示例 2:

set(SRC_LIST main.cc
             rpc/CRNode.cpp
             rpc/Schd_types.cpp
             task/TaskExecutor.cpp
             task/TaskMoniter.cpp
             util/Const.cpp
             util/Globals.cc
   )

add_executable(CRNode ${SRC_LIST})

该示例中,定义了该工程会生成一个名为 CRNode 的可执行文件,所依赖的源文件是变量 SRC_LIST 定义的源文件列表。

如果前面 project() 命令中定义的项目名称也是 CRNode,没有什么问题,两者之间没有任何关系。

add_library

add_library(<name> [STATIC|SHARED|MODULE]
            [EXCLUDE_FROM_ALL]
            [source1] [source2 …])
  • name:库文件名称;
  • [STATIC|SHARED|MODULE]:生成的库的文件类型(静态库/共享库);
  • [EXCLUDE_FROM_ALL]:表示该库不会被默认构建;
  • source1, …, sourceN:生成库所依赖的源文件。

示例:

add_library(hello SHARED ${LIB_hello_SRC})

link_directories

link_directories(directory1 directory2 …)

该命令用于添加外部库的搜索路径。

target_link_libraries

target_link_libraries(<target> … <item>…)
  • target:目标文件;
  • item:链接外部库文件。

指定链接目标文件时需要链接的外部库,效果类似于 GCC 编译参数 -L,解决外部库依赖的问题。

message

message([<mode>] "message to display" …)
  • mode:确定消息的类型:

    模式 描述
    (none) 重要信息
    STATUS 附带信息
    WARNING CMake 警告,继续处理
    AUTHOR_WARNING CMake 警告(dev),继续处理
    SEND_ERROR CMake 错误,继续处理,但跳过生成过程
    FATAL_ERROR CMake 错误,停止处理和生成过程
    DEPRECATION 如果分别启用 CMAKE_ERROR_DEPRECATEDCMAKE_WARN_DEPRECATED,则弃用 CMake 错误或警告,否则无消息
  • "message to display":需要显示的文字消息。

set_target_properties

set_target_properties(target1 target2 …
                      PROPERTIES prop1 value1
                      prop2 value2 …)

设置目标的某些属性,改变它们构建的方式。

该指令为一个目标设置属性,语法是列出所有用户想要变更的文件,然后提供想要设置的值。用户可以使用任何想用的属性与对应的值,并在随后的代码中调用 GET_TARGET_PROPERTY 命令取出属性的值。

影响目标输出文件的属性PROPERTIES详述如下:

PREFIX、SUFFIX

  • PREFIX 覆盖了默认的目标名前缀(如 lib);
  • SUFFIX 覆盖了默认的目标名后缀(如 .so)。

IMPORT_PREFIX、IMPORT_SUFFIX

PREFIXSUFFIX 是等价的属性,但针对的是 DLL 导入库(即共享库目标)。

OUTPUT_NAME

构建目标时,OUTPUT_NAME 用来设置目标的真实名称。

LINK_FLAGS

为一个目标的链接阶段添加额外标志。

LINK_FLAGS_<CONFIG> 将为配置 <CONFIG> 添加链接标志,如 DebugReleaseRelWithDebInfoMinSizeRel

COMPILE_FLAGS

设置附加的编译器标志,在构建目标内的源文件时用到。

LINKER_LANGUAGE

改变链接可执行文件或共享库的工具。默认值是设置与库中文件相匹配的语言。

CXX 与 C 是该属性的公共值。

VERSION、SOVERSION

VERSION 指定构建的版本号,SOVERSION 指定构建的 API 版本号。

构建或安装时,如果平台支持符号链接,且链接器支持 so 名称,那么将会创建恰当的符号链接。

如果只指定两者中的一个,缺失的另一个假定为具有相同版本号。

示例 1:

set_target_properties(hello_static PROPERTIES OUTPUT_NAME "hello")

示例 2:

set_target_properties(hello PROPERTEIES VERSION 1.2 SOVERSION 1)

该命令用于控制版本,VERSION 指代动态库版本,SOVERSION 指代 API 版本。

aux_source_directory

查找某个路径下的所有源文件,并将源文件列表存储到一个变量中。

aux_source_directory(<dir> <variable>)

示例:

aux_source_directory(. SRC_LIST)

该指令将当前目录下的文件列表全部存入变量 SRC_LIST 中。

install

install 命令可以按照对象的不同分为多种类型:目标文件、非目标文件、目录。

目标文件

install(TARGETS targets… [EXPORT <export-name>]
        [[ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|
          PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE]
         [DESTINATION <dir>]
         [PERMISSIONS permissions…]
         [CONFIGURATIONS [Debug|Release|…]]
         [COMPONENT <component>]
         [NAMELINK_COMPONENT <component>]
         [OPTIONAL] [EXCLUDE_FROM_ALL]
         [NAMELINK_ONLY|NAMELINK_SKIP]
        ] […]
        [INCLUDES DESTINATION [<dir> …]]
        )
  • TARGETS targetstargets 即通过 add_executableadd_library 定义的目标文件,可能是可执行二进制文件、动态库、静态库;
  • DESTINATION <dir>dir 即定义的安装路径。安装路径可以是绝对/相对路径。在绝对路径的情况下,CMAKE_INSTALL_PREFIX 就无效了。
    • 如果希望使用 CMAKE_INSTALL_PREFIX 定义安装路径,就需要使用相对路径,这时候安装后的路径就是 ${CMAKE_INSTALL_PREFIX}\\<dir>

非目标文件

.sh 脚本文件,即为典型的非目标文件的可执行程序。

install(<FILES|PROGRAMS> files… DESTINATION <dir>
        [PERMISSIONS permissions…]
        [CONFIGURATIONS [Debug|Release|…]]
        [COMPONENT <component>]
        [RENAME <name>] [OPTIONAL] [EXCLUDE_FROM_ALL])

使用方法和上述目标文件指令的 install 基本相同。唯一的区别是,安装非目标文件之后的权限还包括 OWNER_EXECUTEGOUP_EXECUTEWORLD_EXECUTE,即 755 权限目录的安装。

目录

install(DIRECTORY dirs… DESTINATION <dir>
        [FILE_PERMISSIONS permissions…]
        [DIRECTORY_PERMISSIONS permissions…]
        [USE_SOURCE_PERMISSIONS] [OPTIONAL] [MESSAGE_NEVER]
        [CONFIGURATIONS [Debug|Release|…]]
        [COMPONENT <component>] [EXCLUDE_FROM_ALL]
        [FILES_MATCHING]
        [[PATTERN <pattern> | REGEX <regex>]
         [EXCLUDE] [PERMISSIONS permissions…]] […])
  • DIRECTORY dirsdirs 是所在源文件目录的相对路径。但必须注意,abcabc/ 有很大区别:
    • abc:该目录将被安装为目标路径的 abc
    • abc/:将该目录内容安装到目标路径,但不包括该目录本身。

示例:

install(DIRECTORY icons scripts/ DESTINATION share/myproj
        PATTERN "CVS" EXCLUDE
        PATTERN "scripts/*" PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ)

该指令的执行结果是:

  • 将 icons 目录安装到 <prefix>/share/myproj
  • 将 scripts/ 中的内容安装到 <prefix>/share/myproj
  • 不包含目录名为 CVS 的目录;
  • 对于 scripts/* 文件指定权限为 OWNER_EXECUTEOWNER_WRITEOWNER_READGROUP_EXECUTGROUP_READ

基本控制语法

if

if…else… 语法格式有些类似于 Visual Basic .NET:

if(<expression>)
  # then section.
  COMMAND1(<ARGS> …)
  COMMAND2(<ARGS> …)
  #…
elseif(<expression2>)
  # elseif section.
  COMMAND1(<ARGS> …)
  COMMAND2(<ARGS> …)
  #…
else(<expression>)
  # else section.
  COMMAND1(<ARGS> …)
  COMMAND2(<ARGS> …)
  #…
endif(<expression>)

其中,一定要有 endif()if() 对应。

if 基本用法

  • if(<expression>)expression 不为 0OFFNOFALSENIGNORENOTFOUND、空字符串,或者不含后缀 -NOTFOUND 时,为真;

  • if(NOT <expression>:与上一条相反;

  • if(<expr1> AND <expr2>)

  • if(<expr1> OR <expr2>

  • if(COMMAND command-name):如果 command-name 是命令、宏或函数并可调用,为真;

  • if(EXISTS path-to-file-or-directory):如果给定路径的文件或目录存在,为真;

  • if(file1 IS_NEWER_THAN file2):当 file1 比 file2 新,或 file1/file2 中有一个不存在时为真,文件名需使用全路径;

  • if(IS_DIRECTORY path-to-directory):当给定路径是目录时,为真。注意使用全路径;

  • if(DEFINED <variable>):如果变量已被定义,为真;

  • if(<variable|string> MATCHES regex):当给定变量或字符串能匹配正则表达式 regex 时,为真。此处的 variable 直接使用变量名,而非 ${variable}

    • 示例:

      if("hello" MATCHES "ell") 
      message("true") 
      endif("hello" MATCHES "ell")
      

数字比较表达式

  • if(<var> LESS <number>)
  • if(<var> GREATER <number>)
  • if(<var> EQUAL <number>)

字母表顺序比较

  • if(<var1> STRLESS <var2>)
  • if(<var1> STRGREATER <var2>)
  • if(<var1> STREQUAL <var2>)

示例 1:

判断平台差异。

if(WIN32)
    message(STATUS "This is windows.")
else(WIN32)
    message(STATUS "This is not windows.")
endif(WIN32)

上述代码可以控制不同平台进行不同控制。

也许 else(WIN32) 之类的语句阅读起来很不舒服,这时候可以加上语句:

set(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS ON)

这时候上述结构就可以写成:

if(WIN32)
    message(STATUS "This is windows.")
else()
    message(STATUS "This is not windows.")
endif()

示例 2:

if(WIN32)
    #do something related to WIN32
elseif(UNIX)
    #do something related to UNIX
elseif(APPLE)
    #do something related to APPLE
endif(WIN32)

while

while(<condition>)
  COMMAND1(<ARGS> …)
  COMMAND2(<ARGS> …)
  …
endwhile(<condition>)

类似于 endifwhile() 也需要 endwhile() 匹配。

真假判断条件可以参考 if 指令。

foreach

foreach 有多种使用形式的语法,且每个 foreach() 都需要一个 endforeach() 与之匹配。

列表语法

foreach(<loop_var> <arg1> <arg2> …)
  COMMAND1(<ARGS> …)
  COMMAND2(<ARGS> …)
  …
endforeach(<loop_var>)

示例:

aux_source_directory(. SRC_LIST)
foreach(F ${SRC_LIST})
     message(${F})
endforeach(F)

该示例中,先将当前路径下的所有源文件列表赋值给变量 SRC_LIST,然后遍历 SRC_LIST 中的文件,并持续输出信息,信息内容是当前路径下所有源文件的名称。

范围语法

foreach(<loop_var> RANGE <total>)
  COMMAND1(<ARGS> …)
  COMMAND2(<ARGS> …)
  …
endforeach(<loop_var>)

示例:

foreach(v RANGE 10)
    message(${v})
endforeach(v)

该示例从 0 到 total(此处为 10),以 1 为步进。此处输出为:012345678910

范围步进语法

foreach(<loop_var> RANGE <start> <stop> [<step>])
  COMMAND1(<ARGS> …)
  COMMAND2(<ARGS> …)
  …
endforeach(<loop_var>)

start 开始,到 stop 结束,以 step 为步进。

示例:

foreach(a RANGE 5 15 3)
    message(${a})
endforeach(a)

此处输出为 581114

迭代语法

foreach 还可以循环访问生成的数字范围。这种迭代有三种类型:

  • 指定单个数字时,范围将包含元素 [0, …, total](包括 total);
  • 指定两个数字时,范围将包含从第一个数字到第二个数字(包括)的元素;
  • 第三个可选数字是用于从第一个数字迭代到第二个数字(包括)的增量。
foreach(<loop_var> IN [LISTS [<list1> …]]
                      [ITEMS [<item1> …]])
  COMMAND1(<ARGS> …)
  COMMAND2(<ARGS> …)
  …
endforeach(<loop_var>)

foreach 循环访问一个精确的项列表:

  • LISTS:给要遍历的列表值变量命名,包括空元素(空字符串是零长度列表)。注意宏参数不是变量。
  • ITEMS:结束参数解析并包含迭代中它后面的所有参数。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容