# 11.1 生成源代码和二进制包

**NOTE**:*此示例代码可以在* <https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-11/recipe-01> *中找到。该示例在CMake 3.6版(或更高版本)中是有效的，并且已经在GNU/Linux、macOS和Windows上进行过测试。*

如果代码是开源的，用户将能够下载项目的源代码，并使用完全定制的CMake脚本自行构建。当然，打包操作也可以使用脚本完成，但是CPack提供了更简单和可移植的替代方案。本示例将指导您创建一些包:

* **源代码包**：可以将源代码直接压缩成归档文件，进行发送。用户将不必为特定的版本控制系统操心。
* **二进制包**：工具将新构建的目标以打包的方式到归档文件中。这个功能非常有用，但可能不够健壮，无法发布库和可执行程序。
* **平台原生的二进制安装**：CPack能够以许多不同的格式生成二进制安装程序，因此可以将软件发布到不同的平台。我们将展示如何生成安装程序:
  * 基于Debian的GNU/Linux发行版的`.deb`格式： <https://manpages.debian.org/unstable/dpkg-dev/deb.5.en.html>
  * 基于Red Hat的GNU/Linux发行版的`.rpm`格式： <http://rpm.org/>
  * macOS包的`.dmg`格式: <https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html>
  * Windows的NSIS格式: <http://nsis.sourceforge.net/Main_Page>

## 准备工作

我们将使用第10章第3节的示例，项目树由以下目录和文件组成:

```
.
├── cmake
│    ├── coffee.icns
│    ├── Info.plist.in
│    └── messageConfig.cmake.in
├── CMakeCPack.cmake
├── CMakeLists.txt
├── INSTALL.md
├── LICENSE
├── src
│    ├── CMakeLists.txt
│    ├── hello-world.cpp
│    ├── Message.cpp
│    └── Message.hpp
└── tests
    ├── CMakeLists.txt
    └── use_target
        ├── CMakeLists.txt
        └── use_message.cpp
```

由于本示例的重点是使用CPack，所以不会讨论源码。我们只会在`CMakeCPack.cmake`中添加打包指令。此外，还添加了`INSTALL.md`和`LICENSE`文件：打包要求需要包含安装说明和项目许可信息。

## 具体实施

让我们看看需要添加到这个项目中的打包指令。我们将在`CMakeCPack.cmake`中收集它们，并在在`CMakeLists.txt`的末尾包含这个模块`include(cmakecpackage.cmake)`:

1. 我们声明包的名称，与项目的名称相同，因此我们使用`PROJECT_NAME`的CMake变量:

   ```
   set(CPACK_PACKAGE_NAME "${PROJECT_NAME}")
   ```
2. 声明包的供应商：

   ```
   set(CPACK_PACKAGE_VENDOR "CMake Cookbook")
   ```
3. 打包的源代码将包括一个描述文件。这是带有安装说明的纯文本文件:

   ```
   set(CPACK_PACKAGE_DESCRIPTION_FILE "${PROJECT_SOURCE_DIR}/INSTALL.md")
   ```
4. 还添加了一个包的描述:

   ```
   set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "message: a small messaging library")
   ```
5. 许可证文件也将包括在包中:

   ```
   set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
   ```
6. 从发布包中安装时，文件将放在`/opt/recipe-01`目录下:

   ```
   set(CPACK_PACKAGING_INSTALL_PREFIX "/opt/${PROJECT_NAME}")
   ```
7. CPack所需的主要、次要和补丁版本:

   ```
   set(CPACK_PACKAGE_VERSION_MAJOR "${PROJECT_VERSION_MAJOR}")
   set(CPACK_PACKAGE_VERSION_MINOR "${PROJECT_VERSION_MINOR}")
   set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
   ```
8. 设置了在包装的时候需要忽略的文件列表和目录:

   ```
   set(CPACK_SOURCE_IGNORE_FILES "${PROJECT_BINARY_DIR};/.git/;.gitignore")
   ```
9. 列出了源代码归档的打包生成器——在我们的例子中是`ZIP`，用于生成`.ZIP`归档，`TGZ`用于`.tar.gz`归档:

   ```
   set(CPACK_SOURCE_GENERATOR "ZIP;TGZ")
   ```
10. 我们还列出了二进制存档生成器:

    ```
    set(CPACK_GENERATOR "ZIP;TGZ")
    ```
11. 现在也可声明平台原生二进制安装程序，从DEB和RPM包生成器开始，不过只适用于GNU/Linux:

    ```
    if(UNIX)
      if(CMAKE_SYSTEM_NAME MATCHES Linux)
        list(APPEND CPACK_GENERATOR "DEB")
        set(CPACK_DEBIAN_PACKAGE_MAINTAINER "robertodr")
        set(CPACK_DEBIAN_PACKAGE_SECTION "devel")
        set(CPACK_DEBIAN_PACKAGE_DEPENDS "uuid-dev")

        list(APPEND CPACK_GENERATOR "RPM")
        set(CPACK_RPM_PACKAGE_RELEASE "1")
        set(CPACK_RPM_PACKAGE_LICENSE "MIT")
        set(CPACK_RPM_PACKAGE_REQUIRES "uuid-devel")
      endif()
    endif()
    ```
12. 如果我们在Windows上，我们会想要生成一个NSIS安装程序:

    ```
    if(WIN32 OR MINGW)
      list(APPEND CPACK_GENERATOR "NSIS")
      set(CPACK_NSIS_PACKAGE_NAME "message")
      set(CPACK_NSIS_CONTACT "robertdr")
      set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
    endif()
    ```
13. 另一方面，在macOS上，bundle包是我们的安装程序的选择:

    ```
    if(APPLE)
      list(APPEND CPACK_GENERATOR "Bundle")
      set(CPACK_BUNDLE_NAME "message")
      configure_file(${PROJECT_SOURCE_DIR}/cmake/Info.plist.in Info.plist @ONLY)
      set(CPACK_BUNDLE_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
      set(CPACK_BUNDLE_ICON ${PROJECT_SOURCE_DIR}/cmake/coffee.icns)
    endif()
    ```
14. 我们在现有系统的包装生成器上，向用户打印一条信息:

    ```
    message(STATUS "CPack generators: ${CPACK_GENERATOR}")
    ```
15. 最后，我们包括了`CPack.cmake`标准模块。这将向构建系统添加一个包和一个`package_source`目标:

    ```
    include(CPack)
    ```

现在来配置这个项目：

```
$ mkdir -p build
$ cd build
$ cmake ..
```

使用下面的命令，我们可以列出可用的目标(示例输出是在使用Unix Makefile作为生成器的GNU/Linux系统上获得的):

```
$ cmake --build . --target help

The following are some of the valid targets for this Makefile:
... all (the default if no target is provided)
... clean
... depend
... install/strip
... install
... package_source
... package
... install/local
... test
... list_install_components
... edit_cache
... rebuild_cache
... hello- world
... message
```

我们可以看到`package`和`package_source`目标是可用的。可以使用以下命令生成源包:

```
$ cmake --build . --target package_source

Run CPack packaging tool for source...
CPack: Create package using ZIP
CPack: Install projects
CPack: - Install directory: /home/user/cmake-cookbook/chapter-11/recipe-01/cxx-example
CPack: Create package
CPack: - package: /home/user/cmake-cookbook/chapter- 11/recipe-01/cxx-example/build/recipe-01-1.0.0-Source.zip generated.
CPack: Create package using TGZ
CPack: Install projects
CPack: - Install directory: /home/user/cmake-cookbook/chapter- 11/recipe-01/cxx-example
CPack: Create package
CPack: - package: /home/user/cmake-cookbook/chapter-11/recipe-01/cxx-example/build/recipe-01- 1.0.0-Source.tar.gz generated.
```

同样，也可以构建二进制包:

```
$ cmake --build . --target package message-1.0.0-Linux.deb
```

例子中，最后得到了以下二进制包:

```
message-1.0.0-Linux.rpm
message-1.0.0-Linux.tar.gz
message-1.0.0-Linux.zip
```

## 工作原理

CPack可用于生成用于分发的包。生成构建系统时，我们在`CMakeCPack.cmake`中列出了CPack指令，用于在构建目录下生成`CPackConfig.cmake`。当运行以`package`或`package_source`目标的CMake命令时，CPack会自动调用，参数是自动生成的配置文件。实际上，这两个新目标是对CPack简单规则的使用。与CMake一样，CPack也有生成器的概念。CMake上下文中的生成器是用于生成本地构建脚本的工具，例如Unix Makefile或Visual Studio项目文件，而CPack上下文中的生成器是用于打包的工具。我们列出了这些变量，并对不同的平台进行了特别的关注，为源包和二进制包定义了`CPACK_SOURCE_GENERATOR`和`CPACK_GENERATOR`变量。因此，`DEB`包生成器将调用`Debian`打包实用程序，而`TGZ`生成器将调用给定平台上的归档工具。我们可以直接在`build`目录中调用CPack，并选择要与`-G`命令行选项一起使用的生成器。`RPM`包可以通过以下步骤生成:

```
$ cd build
$ cpack -G RPM

CPack: Create package using RPM
CPack: Install projects
CPack: - Run preinstall target for: recipe-01
CPack: - Install project: recipe-01
CPack: Create package
CPackRPM: Will use GENERATED spec file: /home/user/cmake-cookbook/chapter-11/recipe-01/cxx-example/build/_CPack_Packages/Linux/RPM/SPECS/recipe-01.spec
CPack: - package: /home/user/cmake-cookbook/chapter-11/recipe-01/cxx-example/build/recipe-01-1.0.0-Linux.rpm generated.
```

对于任何发行版，无论是源代码还是二进制文件，我们只需要打包用户需要的内容，因此整个构建目录和其他与版本控制相关的文件，都必须从要打包的文件列表中排除。我们的例子中，排除列表使用下面的命令声明：

```
set(CPACK_SOURCE_IGNORE_FILES "${PROJECT_BINARY_DIR};/.git/;.gitignore")
```

我们还需要指定包的基本信息，例如：名称、简短描述和版本。这个信息是通过CMake变量设置的，当包含相应的模块时，CMake变量被传递给CPack。

**NOTE**:*由于CMake 3.9中的`project()`命令接受`DESCRIPTION`字段，该字段带有一个描述项目的短字符串。CMake将设置一个`PROJECT_DESCRIPTION`，可以用它来重置`CPACK_PACKAGE_DESCRIPTION_SUMMARY`。*

让我们详细看看，可以为示例项目生成的不同类型包的说明。

### 打包源码

我们的示例中，决定对源存档使用`TGZ`和`ZIP`生成器。这些文件将分别生成`.tar.gz`和`.zip`压缩文件。我们可以检查生成的`.tar.gz`文件的内容:

```
$ tar tzf recipe-01-1.0.0-Source.tar.gz

recipe-01-1.0.0-Source/opt/
recipe-01-1.0.0-Source/opt/recipe-01/
recipe-01-1.0.0-Source/opt/recipe-01/cmake/
recipe-01-1.0.0-Source/opt/recipe-01/cmake/coffee.icns
recipe-01-1.0.0-Source/opt/recipe-01/cmake/Info.plist.in
recipe-01-1.0.0-Source/opt/recipe-01/cmake/messageConfig.cmake.in
recipe-01-1.0.0-Source/opt/recipe-01/CMakeLists.txt
recipe-01-1.0.0-Source/opt/recipe-01/src/
recipe-01-1.0.0-Source/opt/recipe-01/src/Message.hpp
recipe-01-1.0.0-Source/opt/recipe-01/src/CMakeLists.txt
recipe-01-1.0.0-Source/opt/recipe-01/src/Message.cpp
recipe-01-1.0.0-Source/opt/recipe-01/src/hello-world.cpp
recipe-01-1.0.0-Source/opt/recipe-01/LICENSE
recipe-01-1.0.0-Source/opt/recipe-01/tests/
recipe-01-1.0.0-Source/opt/recipe-01/tests/CMakeLists.txt
recipe-01-1.0.0-Source/opt/recipe-01/tests/use_target/
recipe-01-1.0.0-Source/opt/recipe-01/tests/use_target/CMakeLists.txt
recipe-01-1.0.0-Source/opt/recipe-01/tests/use_target/use_message.cpp
recipe-01-1.0.0-Source/opt/recipe-01/INSTALL.md
```

与预期相同，只包含源码树的内容。注意`INSTALL.md`和`LICENSE`文件也包括在内，可以通过`CPACK_PACKAGE_DESCRIPTION_FILE`和`CPACK_RESOURCE_FILE_LICENSE`变量指定。

**NOTE**:*Visual Studio生成器无法解析`package_source`目标:*<https://gitlab.kitware.com/cmake/cmake/issues/13058。>

### 二进制包

创建二进制存档时，CPack将打包`CMakeCPack.cmake`中描述的目标的内容。因此，在我们的示例中，hello-world可执行文件、消息动态库以及相应的头文件都将以`.tar.gz`和`.zip`的格式打包。此外，还将打包CMake配置文件。这对于需要链接到我们的库的其他项目非常有用。包中使用的安装目录可能与从构建树中安装项目时使用的前缀不同，可以使用`CPACK_PACKAGING_INSTALL_PREFIX`变量来实现这一点。我们的示例中，我们将它设置为系统上的特定位置:`/opt/recipe-01`。

```
$ tar tzf recipe-01-1.0.0-Linux.tar.gz

recipe-01- 1.0.0-Linux/opt/
recipe-01-1.0.0-Linux/opt/recipe-01/
recipe-01-1.0.0- Linux/opt/recipe-01/bin/
recipe-01-1.0.0-Linux/opt/recipe-01/bin/hello- world
recipe-01-1.0.0-Linux/opt/recipe-01/share/
recipe-01-1.0.0- Linux/opt/recipe-01/share/cmake/
recipe-01-1.0.0-Linux/opt/recipe- 01/share/cmake/recipe-01/
recipe-01-1.0.0-Linux/opt/recipe- 01/share/cmake/recipe-01/messageConfig.cmake
recipe-01-1.0.0- Linux/opt/recipe-01/share/cmake/recipe-01/messageTargets-hello- world.cmake
recipe-01-1.0.0-Linux/opt/recipe-01/share/cmake/recipe- 01/messageConfigVersion.cmake
recipe-01-1.0.0-Linux/opt/recipe- 01/share/cmake/recipe-01/messageTargets-hello-world- release.cmake
recipe-01-1.0.0-Linux/opt/recipe-01/share/cmake/recipe- 01/messageTargets-release.cmake
recipe-01-1.0.0-Linux/opt/recipe- 01/share/cmake/recipe-01/messageTargets.cmake
recipe-01-1.0.0- Linux/opt/recipe-01/include/
recipe-01-1.0.0-Linux/opt/recipe- 01/include/message/
recipe-01-1.0.0-Linux/opt/recipe- 01/include/message/Message.hpp
recipe-01-1.0.0-Linux/opt/recipe- 01/include/message/messageExport.h
recipe-01-1.0.0-Linux/opt/recipe- 01/lib64/
recipe-01-1.0.0-Linux/opt/recipe- 01/lib64/libmessage.so
recipe-01-1.0.0-Linux/opt/recipe- 01/lib64/libmessage.so.1`
```

### 平台原生的二进制安装

我们希望每个平台原生二进制安装程序的配置略有不同。可以在单个`CMakeCPack.cmake`中使用CPack管理这些差异，就像例子中做的那样。

对于GNU/Linux系统，配置了`DEB`和`RPM`生成器:

```
if(UNIX)
  if(CMAKE_SYSTEM_NAME MATCHES Linux)
    list(APPEND CPACK_GENERATOR "DEB")
    set(CPACK_DEBIAN_PACKAGE_MAINTAINER "robertodr")
    set(CPACK_DEBIAN_PACKAGE_SECTION "devel")
    set(CPACK_DEBIAN_PACKAGE_DEPENDS "uuid-dev")

    list(APPEND CPACK_GENERATOR "RPM")
    set(CPACK_RPM_PACKAGE_RELEASE "1")
    set(CPACK_RPM_PACKAGE_LICENSE "MIT")
    set(CPACK_RPM_PACKAGE_REQUIRES "uuid-devel")
  endif()
endif()
```

我们的示例依赖于UUID库，`CPACK_DEBIAN_PACKAGE_DEPENDS`和`cpack_rpm_package_require`选项允许指定，包和数据库中对其他包的依赖关系。可以使用dpkg和rpm程序分别分析生成的`.deb`和`.rpm`包的内容。

注意，`CPACK_PACKAGING_INSTALL_PREFIX`也会影响这些包生成器：我们的包将安装到`/opt/recipe-01`。

CMake真正提供了跨平台和可移植构建系统的支持。下面将使用Nullsoft脚本安装系统(NSIS)创建一个安装程序:

```
if(WIN32 OR MINGW)
  list(APPEND CPACK_GENERATOR "NSIS")
  set(CPACK_NSIS_PACKAGE_NAME "message")
  set(CPACK_NSIS_CONTACT "robertdr")
  set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
endif()
```

如果在macOS上构建项目，将启用`Bundle packager`:

```
if(APPLE)
  list(APPEND CPACK_GENERATOR "Bundle")
  set(CPACK_BUNDLE_NAME "message")
  configure_file(${PROJECT_SOURCE_DIR}/cmake/Info.plist.in Info.plist @ONLY)
  set(CPACK_BUNDLE_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
  set(CPACK_BUNDLE_ICON ${PROJECT_SOURCE_DIR}/cmake/coffee.icns)
endif()
```

macOS的示例中，需要为包配置属性列表文件，这是通过`configure_file`实现的。`Info.plist`的位置和包的图标，这些都可以通过CPack的变量进行设置。

**NOTE**:*可以在这里阅读，关于属性列表格式的更多信息:*<https://en.wikipedia.org/wiki/Property_list>

## 更多信息

对`CMakeCPack.cmake`进行设置，要比列出CPack的配置选项简单的多，我们可以将`CPACK_*`变量的每个生成器设置放在单独的文件中，比如`CMakeCPackOptions.cmake`，并将这些设置包含到`CMakeCPack.cmake`使用`set(CPACK_PROJECT_CONFIG_FILE "${PROJECT_SOUsRCE_DIR}/CMakeCPackOptions.cmake")`将设置包含入`CMakeCPack.cmake`中。还可以在CMake时配置该文件，然后在CPack时包含该文件，这为配置多格式包生成器提供了一种简洁的方法(参见<https://cmake.org/cmake/help/v3.6/module/CPack.html> )。

与CMake中的所有工具一样，CPack功能强大、功能多样，并且提供了更多的灵活性和选项。感兴趣的读者应该看官方文档的命令行界面CPack (<https://cmake.org/cmake/help/v3.6/manual/cpack.1.html> )手册页，如何使用CPack生成器打包相关项目的更多细节(<https://cmake.org/cmake/help/v3.6/module/CPack.html> )。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://chenxiaowei.gitbook.io/cmake-cookbook/11.0-chinese/11.1-chinese.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
