Makefile用法

1. 什么是Makefile

  • Makefile就和shell脚本一样,能自动批量处理文件。Makefile可以对整个工程按给定的规则进行编译,这中对整个工程的自动化编译可以极大地提高软件开发效率。

2. 总的规则

  • make命令需要一个无后缀的Makefile文件,在这个文件书写编译的规则
  • 编译的规则
    1. 如果这个工程没有编译过,则所有的C文件都要编译并被链接
    2. 如果工程中的某几个c文件被修改了,那只需要编译被修改的c文件,并链接目标程序
    3. 如果这个工程的头文件被修改了,那需要编译引用这几个头文件的c文件,并链接目标程序。
  • 如何Makefile写的好,所有的一切只用make命令就可以完成,它能根据修改情况自动重编译。

3. 核心语法

target ...:prerequisites: ...
    command
    ...
    ...
  • target:可以是object file(目标文件),也可以是一个可执行文件,还可以是一个label(标签)
  • prerequisites:生成该target所依赖的文件(target也可以包含在内)
  • command:该target要执行的命令(gcc,shell,等命令)
    总的来说,target这个一个或多个目标文件的生成依赖于prerequisites中的文件,同过command的规则生成。如果prerequisites中的文件比target新,command的命令就会执行。

4. 编译过程

  • 输入make命令,make会在当前目录下寻找名为"Makefile"的文件,
  • 找到后它会找第一个target文件,并将其作为最终的目标文件。
  • 如果该文件不存在,或者所依赖的文件比它新,则执行command的命令生成target文件
  • 如果target的依赖文件不存在,则make会在文件中寻找以该依赖文件为目标文件的依赖关系,再根据规则生成依赖文件。
    整个make根据依赖性组织,make会一层一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在寻找过程中,如果出现错误,例如找不到依赖文件,make会直接退出,并报错。而对于定义的命令的错误,或是编译不成功,make根本不理会,make只关心文件的依赖关系。

5. Makefile的变量

objects = main.o a.o b.o 

target: $(object)
    cc -o target $(object)
  • 如果某些文件被重复使用,可以声明一个变量,和c语言的宏差不多。
  • 变量名 = 文件名 文件名
  • 以 $(变量名) 来使用变量

6. 清空目标文件

  • 每个Makefile文件都应该写一个目标清空的规则,这不仅便于重编译,也利于保持文件的清洁。

7. Makefile文件的结构

  1. 显示规则:显示规则说明了如何生成一个或者多个目标文件,由我们自己指出要生成的文件、文件的依赖文件和生成的命令。
  2. 隐晦规则:make有自动推导的功能
  3. 变量的定义:在Makefile中我们可以定义一系列的变量,变量一般是字符串,像C语言的宏,当Makefile被执行时,变量会被扩展到相应的引用位置上
  4. 指示文件:在Makefile中引用的另一个Makefile文件,就像C语言的include一样。

5 . 注释:Makefile中只有行注释,用#,可以用 \ 转义显示#

8.伪目标

  • 像clean那样,我们并没有生成"clean"这个文件,"伪目标"并不是一个文件,只是一个标签。由于"伪目标"不是一个文件,所有make无法生成它的依赖关系和决定它是否要执行。我们只能通过显示地指明这个"目标",才能让其生效。"伪目标"的取名不能和文件名重名,不然失去了"伪目标"的意义。
  • 用 .PHONY 来显示地指明目标是"伪目标"

9. 常见指令

  • 显示命令:通常make会把要执行的命令行在命令执行前输出到屏幕上,在命令前用@字符,那命令不会显示
    • make -n 或者 make --just-print:只显示命令,不执行命令,这个有利于调试
    • make -s 或者 make --silent 或者 make --quiet:全面禁止命令的显示
  • 命令执行:如果要让上一条命令的结果应用在下一条,应该使用分号分隔。
  • 命令出错:每当命令运行后,make会检测每个命令的返回码,如果命令执行成功make会执行下一条,当规则中的所有命令成功返回后,这个规则就算成功完成。如果一个规则中的某个命令出错(命令退出码非零),make会终止执行当前规则,这有可能终止所有规则的执行。但有时候,命令的错误并不代表执行不下去,我们不希望这种错误导致终止规则的运行。
    • 在Makefile的命令行前加 - :标记为不管命令出不出错都认为是成功的
    • make -i 或者 make --ignore-errors:这是全局的办法,Makefile中的所有命令都会忽略错误。
    • make -k 或者 make --keep-going:如果某个规则中的命令出错了,那么就终止该规则的执行,但继续执行其他规则。

10. 变量

  • 在Makefile中定义的变量,就像C语言中的宏一样,它代表一个文本字符,在Makefile中执行的时候其会自动原模原样地展开在所使用的地方。
  • 命名规则:可以包含字符、数字、下划线(可以是数字开头),但不应该含有 : # = 或者空字符(空格或回车等),变量是大小写敏感的。
  • 变量在命名时需要赋初值,使用时在变量名前加上符,但最好用()或者{}把变量括起来,如果要字符,可以用$$。
  • 多行变量
define 变量名
变量的值
···
endef

11. 条件判断

target: $(object)
条件()
    command
else
    command
endif
  • ifeq(<arg1>,<arg2>) :比较两个参数是否相等
  • ifneq(<arg1>,<arg2>) : 比较两个参数是否不等
  • ifdef 变量名 : 测试变量是否有值
  • ifdef 变量名 : 测试变量是否为空

12. 函数

  • 语法
    • $(<function> <arguments>)
    • ${<function> <arguments>}
  • <function>:函数名
  • <arguments>:参数,参数间以逗号 , 分隔
  • 函数和参数直接用 空格 分隔
  • 函数调用以$开头
  1. 字符串处理函数:
  • subst: $(subst <from>,<to>,<text>)

    • 字符串替换函数
    • 把 <text> 中的<from>字符串替换成<to>
    • 函数返回被替换后的字符串
  • patsubst: $(patsubst <pattern>,<replacement>,<text>)

    • 模式字符串替换函数
    • 查找 <text> 中的单词是否符合模式<pattern>,( 单词以 空格、 Tab、 回车、 换行 分隔),如果匹配,则用 <replacement> 替换。
    • 函数返回被替换后的字符串
  • strip: $(strip <string>)

    • 去空格函数
    • 去掉<string>字串中开头和结尾的空字符
    • 函数返回被去掉空格的字符串
  • findstring: $(findstring <find>,<in>)

    • 查找字符串函数
    • 在字符串<in>中查找<find>字串
    • 如果找到,返回<find>,否则返回空字符串
  • filter: $(filter <pattern...>,<text>)

    • 过滤函数
    • 以<pattern> 模式过滤<text>字符串中的单词,保留符合模式<pattern>的单词,可以有多个模式
    • 返回符合模式<pattern>的字串
  • filter-out: $(filter-out <pattern ...>,<text>)

    • 反过滤函数
    • 以<pattern> 模式过滤<text>字符串中的单词,去除符合模式<pattern>的单词,可以有多个模式
    • 返回不符合模式<pattern>的字串
  • sort: $(sort <list>)

    • 排序函数
    • 给字符串<list>中的单词排序(升序)
    • 返回排序后的字符串
  • word: $(word <n>,<text>)

    • 取单词函数
    • 取字符串<text>中的第<n>个单词(从1开始)
    • 返回字符串<text>中的第<n>个单词,如果<n>比<text>中的单词数大,那么返回空字符串
  • wordlist: $(wordlist <ss>,<e>,<text>)

    • 取单词串函数
    • 从字符串<text>中取从<ss>开始到<e>的单词串,<ss>和<e>是一个数字
    • 返回字符串<text>中从<ss>到<e>的单词字串,如果<ss>比<text>中的单词数要大,则返回空字符串。如果<e>大于<text>的单词数,那么返回从<ss>开始,到<text>结束的单词串
  • words: $(words <text>)

    • 单词个数统计函数
    • 统计<text> 中字符串中的单词个数
    • 返回<text>中的单词数
  • firstword: $(firstword <text>)

    • 首单词函数
    • 取字符串<text> 中的第一个单词
    • 返回字符串<text>的第一个单词
  1. 文件名操作函数
    每个函数的参数字符串都会被当做一个或是一系列的文件名来对待
  • dir: $(dir <names...>)

    • 取目录函数
    • 从文件名序列<names>中取出目录部分。目录部分是指最后一个反斜杠 / 之前的部分,如果没有反斜杠,那么返回 ./
    • 返回文件名序列<names>的目录部分
  • notdir: $(notdir <names...>)

    • 取文件函数
    • 从文件名序列<names>中取出非目录部分。目录部分是指最后一个反斜杠 / 之后的部分
    • 返回文件名序列<names>的非目录部分
  • suffix: $(suffix <names...>)

    • 取后缀函数
    • 从文件名序列<names>中取出各个文件名的后缀
    • 返回文件名序列<names>的后缀序列,如果文件没有后缀,则返回空字符串
  • basename: $(basename <names...>)

    • 取前缀函数
    • 从文件名序列<names>中取出各个文件名的前缀部分
    • 返回文件名序列<names>的前缀序列,如果文件没有前缀,则返回空字符串
  • addsuffix: $(addsuffix <suffix>,<names...>)

    • 加后缀函数
    • 把后缀<suffix>加到<names>中的每个单词后面
    • 返回加过后缀的文件名序列
  • addprefix: $(addprefix <prefix>,<names...>)

    • 加前缀函数
    • 把前缀<prefix>加到<names>中的每个单词后面
    • 返回加过前缀的文件名序列
  • join: $(join <list1>,<list2>)

    • 连接函数
    • 把<list2>中的单词对应地加到<list1>的单词的后面。如果<list1>的单词个数要比<list2>的多,那<list1>中的多出来的单词将保持原样。如果<list2>的单词个数要比<list1>多,那<list2>多出来的单词将被复制到<list1>中
    • 返回连接过后的字符串
  • dir: $(dir <names...>)

    • 取目录函数
    • 从文件名序列<names>中取出目录部分。目录部分是指最后一个反斜杠 / 之前的部分,如果没有反斜杠,那么返回 ./
    • 返回文件名序列<names>的目录部分
  1. foreach函数
  • foreach: $(foreach <var>,<list>,<text>)
    • 用来做循环
    • 把参数<list>中的单词逐一取出放到参数<var>所指定的变量中的,然后再执行<text>所包含的表达式。每次<text>会返回一个字符串,循环过程中,<text>的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值
    • <var> 最好是一个变量名
    • <list>可以是一个表达式
    • <text>中一般会使用<var>这个参数来依次枚举<list>中的单词
  1. if函数
  • if:
    • $(if <condition>,<then-part>)
    • $(if <condition>,<then-part>,<else-part>)
  • if函数可以不包含else,如<condition>返回的为非空字符串,那表达式相当于返回值为真
  • <then-part>和<else-part>只会有一个被计算
  1. call函数
  • call: $(call <expression>,<parm1>,<parm2>,...,<parm>)
  • 唯一一个可以用来创建新的参数化的函数
  • 当make执行这个函数时,<expression>参数中的变量,如(1)、(2)等,会被参数<parm1>、<parm2>、<parm3>依次取代,而<expression>的返回值就是call函数的返回值

6.origin函数

  • origin: $(origin <variable>)
  • 他并不操作变量的值,它只是告诉你你的这个变量是哪里来的
  • <variable>是变量的名字,不应该是引用,所以最好不要在<variable>中使用$字符。
  • origin函数的返回值:
    • undefined:如果<variable>从来没有定义过,origin函数返回这个值undefined
    • default:如果<variable>是一个默认的定义
    • environment:如果<variable>是一个环境变量,并且当Makefile被执行时,-e参数没有被打开
    • file: 如果<variable>这个变量被定义在Makefile中
    • command line:如果<variable>这个变量是被命令行定义的
    • override: 如果<variable>是被override指示符重新定义的
    • automatic:如果<variable>是一个命令运行中的自动化变量
  1. shell函数
contents := $(shell cat foo)
files := $(shell echo *.c)
  • 它的参数就是操作系统的shell命令,shell函数把执行操作系统命令后的输出作为函数返回。
  1. 控制make的函数
    make提供了一些函数来控制make的运行,通常你需要检测一些运行Makefile时的运行时信息,并且根据这些信息来决定是让make继续执行还是停止
  • $(error <text ...>)
    • 产生一个致命错误,<text>是错误信息。error函数不会在一被使用就会产生错误信息,所有如果把其定义在某个变量中,并在后续的脚本中使用这个变量,那也是可以的
  • $(warning <text ...>)
    • 它并不会让make退出,只是输出一段警告信息,而make继续执行。

13. make的运行

1.make的退出码

  • 0:表示执行成功
  • 1: 如果make运行时出现任何错误,返回1
  • 2: 如果使用了make的 -q 选项,并且make使得一些目标不需要更新,那返回2
  1. 指定Makefile
  • 指定一个特殊名字的Makefile
    • make -f 文件名
    • make --file 文件名
    • make --makefile 文件名

3.指定目标

  • 一般make的最终目标是第一个目标,如果要指定完成某个目标,可以在make命令后直接跟目标名

4.伪指令

  • 在Unix世界中,特别是GNU的开源软件发布时,它的Makefile文件中都包含编译,安装,打包等功能,我们可以做个参照
    • all:这个伪指令是所有目标的目标,其功能一般是编译所有的目标
    • clean:这个伪指令功能是删除所有被make创建的文件
    • install:这个伪指令功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去
    • print:这个伪指令的功能是列出改变过的源文件
    • tar:这个伪指令功能是把源程序打包备份,就是一个tar文件
    • dist:这个伪指令的功能是创建一个压缩文件,一般是把tar文件压成z文件或gz文件
    • TAGS:这个伪指令的功能是更新所有的目标,以备完整地重编译使用
    • check和 test:这两个一般是用来测试Makefile的流程
  1. 检查规则
    • make -n
    • make --just-print
    • make --dry-run
    • make --recon
  • 不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些对调试很有帮助
    • make -t
    • make --touch
  • 这个参数的意思是把目标文件的时间更新,但不更改目标文件,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状况
    • make -q
    • make --question
  • 这个参数的行为是找目标的意思,如果目标存在,什么也不输出,当然也不会执行,如果目标不存在,会打印出一条出错信息
    • make -W <file>
    • make --what-if=<file>
    • make --assume-new=<file>
    • make --new-file=<file>
  • 这个参数需要指定一个文件,一般是源文件,make会根据规则推导来运行依赖于这个文件的命令,
  1. make的参数
  • 忽略和其他版本make的兼容性
    • make -b
    • make -m
  • 认为所有的目标都需要更新(重编译)
    • make -B
    • make --always-make
  • 指定读取Makefile的目标,如果有多个 -C 参数,make的解释是后面的路径以前面的作为相对路径,并最后的目录作为被指定目录
    • make -C <dir>
    • make --directory=<dir>
  • 输出make的调试信息
    • make -debug[=<options>]
    • 如果没有参数,就输出最简单的调试信息
    • a:就是all,输出所有的调试信息
    • b:就是basic,只输出简单的调试信息,即输出不需要重编译的目标
    • v:也就是verbose,在b选项的级别之上,输出的信息包括哪个make被解析,不需要重编译的依赖文件
    • i:就是implicit,输出隐含规则
    • j:就是jobs,输出执行规则中命令的详细信息,如命令PID,返回码等
    • m:就是Makefile,输出make读取Makefile,更新Makefile,执行Makefile的信息
  • make -d 相当于 make -debug=a
  • 指明环境变量的值覆盖Makefile中定义的变量的值
    • make -e
    • make --environment-overrides
  • 指定需要执行的Makefile
    • make -f=<file>
    • make --file=<file>
    • make --makefile=<file>
  • 显示帮助信息
    • make -h
    • make --help
  • 在执行时忽略所有的错误
    • make -i
    • make --ignore-errors
  • 指定一个被包含Makefile的搜索目标,可以使用多个 -I 参数来指定多个目标
    • make -I <dir>
    • make --include-dir=<dir>
  • 指同时运行命令的个数,如果没有这个参数,make运行命令时能运行多少就运行多少。如果有一个以上的 -j 参数,那么仅最后一个 -j 才有效
    • make -j [<jobsnum>]
    • make --jobs[=<jobsnum>]
  • 出错也不停止运行,如果生成一个目标失败了,那么依赖于其上的目标就不会被执行
    • make -k
    • make --keep-going
  • 指定make运行命令的负载
    • make -l <load>
    • make --load-average[=<load>]
    • make -max-load[=<load>]
  • 仅输出执行过程中的命令序列,但并不执行
    • make -n
    • make --just-print
    • make --dry-run
    • make --recon
  • 不重新生成指定的<file>,即使这个目标的依赖文件新于它
    • make -o <file>
    • make --old-file=<file>
    • make --assume-old=<file>
  • 输出Makefile中的所有数据,包括所有规则和变量,这个参数会让一个简单的Makefile都会输出一堆信息。如果只想输出信息而不想执行Makefile,可以使用 make -qp 的命令。如果想查看执行Makefile前的预设变量和规则,可以使用 make -p -f /dev/null。这个参数输出的信息包含你的Makefile文件的文件名和行号,所以这个参数调试Makefile会很有用。
    • make -p
    • make --print-data-base
  • 不运行命令,也不输出,仅仅检查所指定的目标是否需要更新,如果是0,则说明要更新,如果是2,则说明有错误发生
    • make -q
    • make --question
  • 禁止make使用任何隐含规则
    • make -r
    • make --no-builtin-rules
  • 禁止make使用任何作用于变量上的隐含规则
    -make -R
    • make --no-builtin-variables
  • 在命令运行时不输出命令的输出
    • make -s
    • make --silent
    • make --quiet
  • 取消 -k 选项的作用,因为有些时候make的选项是从环境变量"MAKEFLAGS"中继承下来的,所以可以在命令行中使用这个参数来让环境变量中的 -k 选项失效
    • make -S
    • make --no-keep-going
    • make --stop
  • 只是把目标的修改日期变成最新的,也就是阻止生成目标的命令运行
    • make -t
    • make --touch
  • 输出make程序的版本、版权等关于make的信息
    • make -v
    • make --version
  • 输出运行Makefile之前和之后的信息,这个参数对于跟踪嵌套式调用make时很有用
    • make -w
    • make --print-directory
  • 禁止 -w 选项
    • make --no-print-directory
  • 假定目标<file>需要更新,如果 -n 选项使用,那么参数会输出该目标更新时的运行动作,如果没有 -n ,那就像运行Unix的 touch 命令一样,使得 <file>的修改时间为当前时间
    • make -W <file>
    • make --what-if=<file>
    • make --new-file=<file>
    • make --assume-file=<file>
  • 只有make发现有未定义的变量,就输出警告信息
    • make --warn-undefined-variables

14. 隐含规则

  • 如何不明确地写下规则,make就会在这些预先设置的隐含规则中寻找所需要规则
  • make -r 或 make --no-builtin-rules 可以取消部分预设的隐含规则
  • 常见的隐含规则
  1. 编译C程序的隐含规则
    • <n>.o 的目标的依赖目标会自动推导为 <n>.c ,并且其生成命令时 (CC) -c(CPPFLAGS) $(CFLAGS)
  2. 编译C++程序的隐含规则
    • <n>.o 的目标的依赖目标会自动推导为 <n>.cc 或者 <n>.c ,并且其生成命令是 (CXX) -c(CPPFLAGS) $(CFLAGS)
  3. 汇编和汇编预处理的隐含规则
    • <o>.o 的目标的依赖目标会自动推导为<n>.s ,默认使用编辑器 as,生成命令是:(AS)(ASFLAGS) 。<n>.s 的目标的依赖目标会自动推导为<n>.S,默认使用C预编译器cpp,其生成命令是:(AS)(ASFLAGS)
  4. 链接Object文件的隐含规则
    • <n>目标依赖于<n>.o,通过运行C的编辑器来运行链接程序生成(一般是ld),其生成命令是:(CC)(LDFLAGS) <n>.o (LOADLIBES)(LDLIBS)。这个规则只对一个源文件的工程有效,同时也对多个object文件的也有效
  • 隐含规则使用的变量
    • 在隐含规则中基本上使用了一些预先设置的变量,可以在Makefile中改变这些变量的值,或者在make的命令行中传入这些值,或在你的环境变量中设置这些值
    • 可以用 make -R 或者 make --no-builtin-variables 参数来取消所定义的变量对隐含规则的作用
    • 隐含规则使用的变量可以分成:和命令相关的 或 和参数相关
  • 关于命令的变量
    • AR:函数库打包程序。默认命令是ar
  • 关于命令参数的变量
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 228,835评论 6 534
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,676评论 3 419
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 176,730评论 0 380
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,118评论 1 314
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,873评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,266评论 1 324
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,330评论 3 443
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,482评论 0 289
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,036评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,846评论 3 356
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,025评论 1 371
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,575评论 5 362
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,279评论 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,684评论 0 26
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,953评论 1 289
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,751评论 3 394
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,016评论 2 375

推荐阅读更多精彩内容