从0开始做一个完整项目 -- 软件跨平台编译打包全流程
从0开始做一个完整项目 – 软件跨平台编译打包全流程
背景
当我们有一个软件,想要将这个软件在各个平台运行的时候,我们应该如何实现这个目标。
简单来说,一个软件想要从源代码到用户拿到手可以直接运行,这其中有这么几个阶段
-
编译成可执行文件。
-
将所有依赖库和编译出来的可执行文件打包到一起。
-
通过不同的方式分发给用户。用户进行使用。
其中对于程序员来说,步骤1和2是我们最关注的。所以接下来就针对这两个步骤进行解释。
编译阶段
在跨平台编译的过程中,因为编译器的编译是和一下几个因素关联的,所以一下任何一个因素的差异,都极有可能导致编译失败。
-
Operat System:不同的系统,对于我们来说最直观的区别就是,可执行程序后缀不一样,这实际上是每个系统的可执行程序的结构定义不同导致的。同样不一样的,还有库的。不过关于在什么平台生成对应的程序或者库,我们不需要关注这些细节,我们需要关注的是下面几个。
1. 系统API不同。有很多的系统,提供了不同的系统API给我们,比如
windows平台:
CreateThread\CreateWindow\LoadLibrary
Linux平台:
pthread_create\XOpenDisplay\dlopen
Macos平台:
NsApplication\NsWindow
对于这种问题,只有两种解决办法,
第一种,直接使用这样的宏,来区分不同的系统,在不同系统使用对应的Api
#ifdef _WIN32 // Windows specific code #elif defined(__unix__) // Unix specific code #elif defined(__APPLE__) // macOS specific code #endif
第二种方式,使用std库,因为std库是按照统一的标准、相同的接口来实现的,所以我们的源码调用了相同的std库,我们不用担心这个会导致编译失败。比如将CreateThread和pthread_create的接口统一改成std::thread库等。
第三种方式,使用一层抽象层,所有的调用都是使用抽象层,但是具体的实现,根据平台不同,在CMake的链接阶段,增加判断,根据不同平台使用不同的的实现。比如
# 通用的头文件 set(SOURCES ${SOURCES} openglgenericrenderwindow.h) # 根据不同平台 选择不同的cpp文件进行编译 ifdef _WIN32 set(SOURCES ${SOURCES} windows_openglgenericrenderwindow.cpp) #elif defined(__unix__) set(SOURCES ${SOURCES} unix_openglgenericrenderwindow.cpp) #elif defined(__APPLE__) set(SOURCES ${SOURCES} macos_openglgenericrenderwindow.cpp) #endif
2. **库不同**。每个系统下,动态库静态库的格式都不是一样的,我们在编译阶段不需要关注库的具体格式,因为CMake都已经帮我们完成这些细节了,我们应该关注的是链接的方式,在Windows和Linux平台下,我们可以使用-L(指定库的所在目录) 搭配-l(具体的库的名称)的方式去指定链接哪一个库,
set(CMAKE_EXE_LINKER_FLAGS “CMAKEEXELINKERFLAGS−L{CMAKE_EXE_LINKER_FLAGS} -LCMAKEEXELINKERFLAGS−L{PROJECT_SOURCE_DIR}/external/lib -lmylib”)推荐使用CMake原生的方式:
target_link_libraries(my_exe PRIVATE ${PROJECT_SOURCE_DIR}/external/lib/libmylib.a)Windows:
target_link_libraries(my_exe PRIVATE ${PROJECT_SOURCE_DIR}/external/lib/mylib.lib)```
但是如果在Macos平台下,会有特殊的库目录结构platform,这个可以参考我的[另一篇博客](记录一次难搞的编译错误-- qml-rust 项目编译无法找到QtCore库的问题_qt.core’ not found-CSDN博客)
3. 环境变量不同。不同的操作系统,环境变量是不一样的。同时程序运行所要用到的环境变量也可能会因为系统不同而有所差异。简单来说,当我们的程序启动的时候,操作系统会去找寻程序所要用到的库,从哪里找呢? 默认来说,是启动的程序的当前目录,然后是在系统变量中寻找,在Linux和Macos平台下,是LD_LIBRARY_PATH(静态库)和DYLD_LIBRARY_PATH(动态库),在Windows平台下,PATH变量的下属目录就是默认的搜寻路径。在编译的时候,有时候会因为这些变量导致编译出现问题。 -
项目管理方式不同: 目前来说,针对C++项目,有多种项目管理方式,这些不同的管理方式也导致了在跨平台时的不同行为。
1. MakeFile: 最基础的项目管理方式,这种的和最底层的GCC/CC 做交互,需要手写对应的编译命令。
2. CMake: CMake是在MakeFile的基础上,帮我们实现了具体的编译命令,我们只需要关注CMake中的参数即可。
3. QMake:Qmake是在CMake的基础上,针对Qt的一些库等等进行了一些优化,比如直接Qt += Gui 就相当于在CMke中做了
find_package(Qt{Qt_Target_Version} COMPONENT Gui REQUIRED) target_link_linraries(${Project_Name} PRIVATE Qt{Qt_Target_Version}::Gui )
简化了更多操作。 -
编译器不同: 现有的编译器,主要是MSVC/CLANG/GCC这些,但是这些编译器对于C++标准的完成度可能不同。同时对于某些标准的实现细节也可能不同,所以相同的代码,放在不同的编译器也可能会出现编译不过的问题(比如rttr这个库)。针对不同的编译器,可以在CMake中增加对应的判断
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") add_compile_options(/std:c++17 /W4) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /NODEFAULTLIB:library") elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") add_compile_options(-std=c++17 -Wall -Wextra -Wpedantic) elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") add_compile_options(-std=c++17 -Wall -Wextra -Wpedantic) else() message(FATAL_ERROR "Unsupported compiler") endif()
来解决出现的问题。
打包阶段
在编译完成之后,我们的目标文件xxx.run 已经编译出来了,如果我们需要运行这个程序,实际上操作系统是帮我们隐藏了很多细节的。比如如果我们这个程序依赖了某些库,当我们加载这些库的时候,比如程序初始化的时候会加载静态库,操作系统会从具体的路径找这些库给这个正在运行的程序。但是如果我们给用户使用的话,这些依赖的库,基本来说是不可能出现在用户电脑中的,所以我们需要将我们的可执行程序,用到的所有依赖库和程序一起打包,统一分发给用户。那么如何针对不同的平台去找需要的依赖库呢?
-
Windows平台:
检查依赖库的工具:Dependency Walker(depends.exe
)
可以直接使用Windeployqt工具,将对应的依赖库打包进来。 -
Linux平台:
依赖库检测工具:ldd
Linux平台有多种可执行格式。deb是安装包,Appimage是可直接运行的格式。还有其他的格式就不说了。
可直接使用LinuxDeployQt工具。
linuxdeployqt-continuous-x86_64.AppImage ~/QtProjects/MyApp/AppDir/usr/bin/myapp -appimage
-
Macos平台:
依赖库检测工具:MacDeployQt
注意:如果有用到qml,需要增加–qmldir选项,将对应的qml模块目录拷贝过来,否则程序会无法运行。