CMake入门

文章翻译自:CMake Tutorial

第一步 | 第二步 | 第三步 | 第四步 | 第五步 | 第六步 | 第七步

下面会一步一步的来教你如何使用CMake完成软件的构建系统。大部分章节在Mastering CMake都有专门的介绍,但是把他们整理在一起还是非常有用的。本教程可以在CMake的源码目录中的Tests/Tutorial目录找到。每一步都有它自己的子目录,子目录有完整的源代码。

<a name="s1" id="s1"></a>

基本使用方法(第一步)

最常见的工程都是从源代码文件构建出可执行程序。最简单的CMakeLists.txt文件只需要两行,我们就从这个文件开始。CMakeLists 文件是这样的。

cmake_minimum_required (VERSION 2.6)
project(Tutorial)
add_executable(Tutorial tutorial.cxx)

需要说明一下本例程中CMakeLists文件都使用小写的命令。对于大写,小写,大小写混合的命令书写方法,CMake都支持。tutorial.cxx的源码用来计算一个数字的平方根,第一个版本非常简单,如下:

// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main(int argc, char *argv[])
{
    if (argc < 2) {
        printf("Usage: %s number\n", argv[0]);
        return 1;
    }
    double inputValue = atof(argv[1]);
    double outputValue = sqrt(inputValue);
    printf("The square root of %g is %g\n", inputValue, outputValue);
    return 0;
}

假如CMakeList.txt和tutorial.cxx文件都存放在~/cmake_tutorial目录,使用如下命令就可以构建出tutorial执行文件:

# mkdir ~/cmake_tutorial/build
# cd ~/cmake_tutorial/build
# cmake -G "Unix Makefiles" ..
# make 
# ./Tutorial 9
The square root of 9 is 3

添加版本号和配置头文件

我们给工程添加的第一个功能就是版本号。当然了,你也可以专门修改源代码,但是通过CMakeLists有更大的灵活性。为了增加版本号,CMakeLists 文件如下:

cmake_minimum_required (VERSION 2.6)
project(Tutorial)

# The version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)

# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
    "${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
    "${PROJECT_BINARY_DIR}/TutorialConfig.h"
    )

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")
     
# add the executable
add_executable(Tutorial tutorial.cxx)

因为我们把配置文件写入到了二进制目录中,所有,我们需要把二进制目录加入到包含目录中。在源代码目录中,我们新建TutorialConfig.h.in文件,内容如下:

// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@

CMake 会使用 CMakeLists 文件中的Tutorial_VERSION_MAJORTutorial_VERSION_MINOR 值替换配置文件的@Tutorial_VERSION_MAJOR@@Tutorial_VERSION_MINOR@。接下来,我们修改Tutorial.cxx,让它包含配置头文件,使用版本号。最后的源代码文件如下:

// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"

int main(int argc, char *argv[])
{
    if (argc < 2) {
        printf("%s Version %d.%d\n",
            argv[0],
            Tutorial_VERSION_MAJOR,
            Tutorial_VERSION_MINOR);
        printf("Usage: %s number\n", argv[0]);
        return 1;
    }
    double inputValue = atof(argv[1]);
    double outputValue = sqrt(inputValue);
    printf("The square root of %g is %g\n", inputValue, outputValue);
    return 0;
}

主要的修改就是包含了TutorialConfig.h头文件,在使用消息中打印了版本号。
执行的命令如下:

# cmake .. 
# make
# ./Tutorial
Tutorial Version 1.0
Usage: Tutorial number

<a name="s2" id="s2"></a>

添加一个库(第二步)

现在我们添加一个库到工程中。这个库是我们自己实现的求平方根。可执行文件使用我们自己的库替换由编译器提供的求平方根函数。在这个教程中,我们把库源文件放到MathFunctions,这个单独的目录中。只需要一行的CMakeLists就足够了:

add_library(MathFunctions mysqrt.cxx)

mysqrt.cxx文件只有一个函数叫mysqrtmysqrt和编译器提供的sqrt提供相同的功能。为了使用新库,我们在顶层的CMakeLists文件中调用add_subdirectory,这样库才会被编译。我们还需要把MathFunctions目录添加到包含目录中,这样才能找到MathFunctions/mysqrt.h文件。最后,还需要把新库添加到可执行文件中。最终的CMakeLists的最后几行是这样的:

include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions) 
     
# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial MathFunctions)

现在我们考虑使用MathFunctions作为可选项。在本教程中,没有理由需要这么做,但是,对于更大的库或依赖第三方代码的库,你可能就原因这么做了。第一步在顶层CMakeLists文件中添加一个选项:

# should we use our own math functions?
option (USE_MYMATH 
        "Use tutorial provided math implementation" ON) 

这会在CMake GUI中显示默认值为ON的选项,用户可以根据需要修改。这个配置会存储在缓存中,在用户下次CMake工程时,不需要重新配置这个选项。下一个修改是,通过配置确定是否需要编译、链接MathFunctions。我们把顶层CMakeLists文件的最后几行修改成这样:

# add the MathFunctions library?
#
if (USE_MYMATH)
  include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
  add_subdirectory (MathFunctions)
  set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)
 
# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial  ${EXTRA_LIBS})

使用USE_MYMATH配置来确定是否需要编译和使用MathFunctions。使用变量(本教程中的EXTRA_LIBS)来收集可选库,然后,链接进可执行文件中。这是一个通用的方法来保持有很多可选库的大工程的清晰性。对源代码的修改已经很直截了当了:

// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"
#ifdef USE_MYMATH
#include "MathFunctions.h"
#endif
 
int main (int argc, char *argv[])
{
  if (argc < 2)
    {
    printf("%s Version %d.%d\n", argv[0],
            Tutorial_VERSION_MAJOR,
            Tutorial_VERSION_MINOR);
    printf(stdout,"Usage: %s number\n",argv[0]);
    return 1;
    }
 
  double inputValue = atof(argv[1]);
 
#ifdef USE_MYMATH
  double outputValue = mysqrt(inputValue);
#else
  double outputValue = sqrt(inputValue);
#endif
 
  printf("The square root of %g is %g\n",
          inputValue, outputValue);
  return 0;
}

在源码中,我们同样使用了USE_MYMATH。这是通过在TutorialConfig.h.in配置文件中添加下面的内容实现的:

#cmakedefine USE_MYMATH

<a name="s3" id="s3"></a>

安装和测试(第三步)

在这一步中,我们给工程添加安装规则和测试支持。安装规则已经很明显了。对于MathFunctions库,我们需要安装库文件和头文件,在MathFunctions/CMakeLists中间添加下面两行:

install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)

对于程序文件,我们在顶层CMakeLists文件中添加下面几行来安装可以执行文件和配置文件:

# add the install targets
install (TARGETS Tutorial DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"        
         DESTINATION include)

到此已经全部做完了。现在,你可以构建本教程,执行make install(或在IDE中构建INSTALL目标),将会安装合适的头文件、库文件、可执行文件。CMake变量CMAKE_INSTALL_PREFIX用来确定文件安装的根目录。添加测试也已经很明显了。在顶层CMakeLists文件的最后,我们使用一些计算好的数字来验证程序是否正确运行:

# does the application run
add_test (TutorialRuns Tutorial 25)
 
# does it sqrt of 25
add_test (TutorialComp25 Tutorial 25)
 
set_tests_properties (TutorialComp25 
  PROPERTIES PASS_REGULAR_EXPRESSION "25 is 5")
 
# does it handle negative numbers
add_test (TutorialNegative Tutorial -25)
set_tests_properties (TutorialNegative
  PROPERTIES PASS_REGULAR_EXPRESSION "-25 is 0")
 
# does it handle small numbers
add_test (TutorialSmall Tutorial 0.0001)
set_tests_properties (TutorialSmall
  PROPERTIES PASS_REGULAR_EXPRESSION "0.0001 is 0.01")
 
# does the usage message work?
add_test (TutorialUsage Tutorial)
set_tests_properties (TutorialUsage
  PROPERTIES 
  PASS_REGULAR_EXPRESSION "Usage:.*number")

第一个测试简单的验证程序是否可以执行,有没有段错误或其他原因导致的崩溃,并且返回值为0。这个是最基本的CTest测试。接下来几个测试都使用PASS_REGULAR_EXPRESSION测试属性来验证程序的输出是否满足特定的规则。这些测试案例都是用来验证程序计算出的平方根是否是正确的和在错误参数时,是否打印出使用方法。如果,你需要验证更多的数字,你可以考虑使用下面的宏:

#define a macro to simplify adding tests, then use it
macro (do_test arg result)
  add_test (TutorialComp${arg} Tutorial ${arg})
  set_tests_properties (TutorialComp${arg}
    PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endmacro (do_test)
 
# do a bunch of result based tests
do_test (25 "25 is 5")
do_test (-25 "-25 is 0")

每次调用do_test都会添加名称、输入、结果基于参数的测试案例。

<a name="s4" id="s4"></a>

添加系统内省(第四步)

接下来,让我们根据目标平台是否支持某些特性,来增加一些代码到我们的工程。比如这个例子,我们通过判断目标平台是否支持logexp函数,来确定是否启用代码。当然了,几乎所有的平台都支持这些函数,但对于本教授假设它们是不太常见的。如果平台有log函数,我们在mysqrt中用于记录平方根。我们首先在最上层的CMakeLists.txt里面,使用CheckFunctionExists.cmake宏来检测是否有这些行数。

# does this system provide the log and exp functions?
include (CheckFunctionExists)
check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)

接着我们在TutorialConfig.h.in定义以下变量

// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP

一定要在logexp检测完成后,才能使用configure_file命令。configure_file命令使用当前的设置生成配置文件。最后,我们根据平台是否存在logexp函数,来完成不同的mysqrt函数

// if we have both log and exp then use them
#if defined (HAVE_LOG) && defined (HAVE_EXP)
  result = exp(log(x)*0.5);
#else // otherwise use an iterative approach
  . . .

<a name="s5" id="s5"></a>

添加一个生成的文件和文件生成器(第五步)

在本章节中,我们将向你展示如何在软件的构建过程中添加一个生成的源文件。在这个例子中,我们将把预先计算好平方根的表格添加到构建过程中,并且把表格编译进软件中。为了完成这个例子,我们首先需要一个程序来生成表格。在MathFunctions的子目录,新建文件MakeTable.cxx来生成表格。

// A simple program that builds a sqrt table 
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
 
int main (int argc, char *argv[])
{
  int i;
  double result;
 
  // make sure we have enough arguments
  if (argc < 2)
    {
    return 1;
    }
  
  // open the output file
  FILE *fout = fopen(argv[1],"w");
  if (!fout)
    {
    return 1;
    }
  
  // create a source file with a table of square roots
  fprintf(fout,"double sqrtTable[] = {\n");
  for (i = 0; i < 10; ++i)
    {
    result = sqrt(static_cast<double>(i));
    fprintf(fout,"%g,\n",result);
    }
 
  // close the table with a zero
  fprintf(fout,"0};\n");
  fclose(fout);
  return 0;
}

另外,这个表格是有效的C++代码,表格保存的文件名通过命令行参数传递。接着我们在MathFunctions的CMakeLists.txt文件中添加合适的命令来生成MakeTable可执行文件,并且在构建过程中执行它。需要几个命令来实现这个目的,如下所示:

# first we add the executable that generates the table
add_executable(MakeTable MakeTable.cxx)
 
# add the command to generate the source code
add_custom_command (
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  DEPENDS MakeTable
  )
 
# add the binary tree directory to the search path for 
# include files
include_directories( ${CMAKE_CURRENT_BINARY_DIR} )
 
# add the main library
add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h  )

首先添加命令生成MakeTable的可执行文件。然后添加自定义命令指定如何使用MakeTable生成Table.h文件。接着,通过把生成的文件Table.h添加到MathFunctions库的源文件列表,我们让CMake知道mysqrt.cxx依赖生成的文件Table.h。我们还需要把当前二进制目录添加到包含目录列表中,所以mysqrt.cxx才能包含Table.h,并且在包含目录中发现Table.h。当工程被构建时,首先构建MakeTable可执行文件,接着运行MakeTable生成Table.h。最后,编译包含Table.h的mysqrt.cxx文件生成MathFunctions库。这时,所有功能已完成的最上层的CMakeList.txt文件看起来是这样的:

cmake_minimum_required (VERSION 2.6)
project (Tutorial)
include(CTest)
 
# The version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)
 
# does this system provide the log and exp functions?
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
 
check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)
 
# should we use our own math functions
option(USE_MYMATH 
  "Use tutorial provided math implementation" ON)
 
# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
  "${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
  "${PROJECT_BINARY_DIR}/TutorialConfig.h"
  )
 
# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories ("${PROJECT_BINARY_DIR}")
 
# add the MathFunctions library?
if (USE_MYMATH)
  include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
  add_subdirectory (MathFunctions)
  set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)
 
# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial  ${EXTRA_LIBS})
 
# add the install targets
install (TARGETS Tutorial DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"        
         DESTINATION include)
 
# does the application run
add_test (TutorialRuns Tutorial 25)
 
# does the usage message work?
add_test (TutorialUsage Tutorial)
set_tests_properties (TutorialUsage
  PROPERTIES 
  PASS_REGULAR_EXPRESSION "Usage:.*number"
  )
 
 
#define a macro to simplify adding tests
macro (do_test arg result)
  add_test (TutorialComp${arg} Tutorial ${arg})
  set_tests_properties (TutorialComp${arg}
    PROPERTIES PASS_REGULAR_EXPRESSION ${result}
    )
endmacro (do_test)
 
# do a bunch of result based tests
do_test (4 "4 is 2")
do_test (9 "9 is 3")
do_test (5 "5 is 2.236")
do_test (7 "7 is 2.645")
do_test (25 "25 is 5")
do_test (-25 "-25 is 0")
do_test (0.0001 "0.0001 is 0.01")

TutorialConfig.h.in是这样的:

// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
#cmakedefine USE_MYMATH
 
// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP

MathFunctions的CMakeLists.txt如下:

# first we add the executable that generates the table
add_executable(MakeTable MakeTable.cxx)
# add the command to generate the source code
add_custom_command (
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  DEPENDS MakeTable
  COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  )
# add the binary tree directory to the search path 
# for include files
include_directories( ${CMAKE_CURRENT_BINARY_DIR} )
 
# add the main library
add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h)
 
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)

<a name="s6" id="s6"></a>

构建安装包(第六步)

接下来,假设我们想要把我们的工程分发给其他人,以便他们可以使用它。我们想要在不同的平台分发二进制和源码。这里的安装和我们在之前的章节安装和测试(第三步)中的是有些区别的,在第三步我们编译源代码后,直接安装二进制文件。在本例子中,我们将要构建二进制安装包,可以在包管理系统中使用,例如:cygwin,debian,RPMs等等。我们使用CPack来创建平台相关的安装包。我们需要在最上层的CMakeLists.txt的尾部添加一些代码:

# build a CPack driven installer package
include (InstallRequiredSystemLibraries)
set (CPACK_RESOURCE_FILE_LICENSE  
     "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set (CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
include (CPack)

做起来很简单。我们从包含InstallRequiredSystemLibraries开始。这个模块会包含当前平台需要使用的运行时库。接下来我们设置一些CPack变量,比如:版权许可文件目录,软件版本信息。在本教程的开始部分,我们已经设置好了版本信息。最后我们包含CPack模块,它会使用这些变量和其他的系统属性去生成安装包。
  下一步,正常的构建工程,然后运行CPack。建立一个二进制分发,你可以运行:

cpack --config CPackConfig.cmake

建立一个源码分发,你可以运行:

cpack --config CPackSourceConfig.cmake

<a name="s7" id="s7"></a>

添加仪表盘(Dashboard)支持(第七步)

把我们的测试结果提交到仪表盘是非常简单的。在本教程前面的步骤中,我们已经定义了一些测试用例。我们只需要运行这些测试用例,并且提交结果到仪表盘。为了支持仪表盘,我们需要在最上层的CMakeLists.txt中包含CTest模块。

# enable dashboard scripting
include (CTest)

我们需要新建CTestConfig.cmake,在文件中定义本项目在仪表盘中的名称。

set (CTEST_PROJECT_NAME "Tutorial")

CTest在执行时会读取这个文件。你可以使用CMake为你的项目创建一个简单的仪表盘,把目录切换到二进制目录,然后运行ctest -D Experimental。你的仪表盘的结果会提交到Kitware的公共仪表盘这里

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

推荐阅读更多精彩内容

  • Cmake 入门 前言 CMake是一个比make更高级的编译配置工具,它可以根据不同平台、不同的编译器,生成相应...
    渝潼不肖生阅读 1,991评论 1 5
  • CMake学习 本篇分享一下有关CMake的一些学习心得以及相关使用。 本文目录如下: [1、CMake介绍] [...
    AlphaGL阅读 12,158评论 11 79
  • 注:首发地址 1. 前言 当在做 Android NDK 开发时,如果不熟悉用 CMake 来构建,读不懂 CMa...
    cfanr阅读 23,715评论 1 53
  • 老来平平过,无病无灾祸。 儿孙个个贤,钱财算什么! 老来先赋闲,莫等退休后。 素食主三餐,难得老来瘦。 起床迎日出...
    叶禄青阅读 981评论 0 0
  • 兔子先生和兔子小姐她们不是情侣,是最好的闺蜜,互相关心,互相照顾。一个性格开朗,一个内敛,互相填补着彼此的不足。 ...
    兔子先生和兔子小姐阅读 2,587评论 1 1