【开源软件移植】nomacs开源项目适配鸿蒙 PC 全流程笔记实战

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

本文成果:nomacs 3.17.2295(一款 2969 stars 的跨平台图片查看器)成功交叉编译为鸿蒙 PC(HarmonyOS NEXT,arm64-v8a)原生共享库 libnomacs.so(4.9 MB、ARM aarch64、零非 Qt 系统依赖、链接到 Qt-OHOS 5.12.12),并打包成 HAP 工程 nomacsOhos 在华为 MateBook Pro(HAD-W24)上已成功运行

注意(本部分必看):
开始本文工作前需要完成:
从0创建项目指南,新手先看这优质的实战文章篇,,后续工作都在用HAP 壳工程:
https://blog.csdn.net/weixin_52908342/article/details/161343743
准备 OpenHarmony SDK
准备 Qt for HarmonyOS
复现最小 Qt Widgets Demo
准备 DevEco HAP 壳工程

完成上面步骤,即可跟着本文进行手把手教学适配nomacs!
在这里插入图片描述
在这里插入图片描述

本项目开源地址:https://atomgit.com/weixin_52908342/OH-nomacs

0. 写在前面

nomacs 是一款用 C++ + Qt 写的"小而美的免费图片查看器",社区维护近 12 年(最早可追溯到 2011 年),跨 Windows / Linux / macOS 三端,支持 JPEG / PNG / BMP / GIF / SVG / TIFF / RAW / HEIF / AVIF 等十几种图片格式 —— 是 Linux 桌面上最常被推荐的"看图三件套"之一(另外两个是 gThumb 和 Geeqie)。

把 nomacs 搬到鸿蒙 PC 上的意义有三:

  1. 填补鸿蒙 PC 的"通用图片查看器"工具链 —— 目前鸿蒙生态系统应用偏向"相册"+"详情页"模式,缺一款支持目录浏览、缩放/旋转/全屏、批量定位的桌面级 GUI;
  2. 首次实战"用 stub 头文件 + 整文件空实现 cpp 双重剥离非必要 C++ 库依赖" —— 上游强依赖 exiv2(一个写 EXIF 元数据的库,需要联动 zlib / expat / libxml2 一整堆),重新交叉编译这条依赖链至少要 1~2 天,本次用 stub 法 30 分钟搞定;
  3. 沉淀 Qt 5.14+/5.15+ → 5.12 API 降级清单 —— 前几篇 NitroShare / glogg / DiffPDF 都是 Qt 5.12 友好的"老项目",nomacs 是首次遇到"项目用了 Qt 5.14+ 新 API、但鸿蒙 Qt 还停在 5.12.12"的情况,这条降级清单未来可复用到所有"较新 Qt 项目"的鸿蒙移植。

整个适配过程严格按照下面 8 个阶段推进:

项目侦察 → 下载源码 → 改造方案设计 → CMake 顶层重写
   → exiv2 双重 stub → Qt API 降级 5 处 → 编译 + 5 项产物自检 → HAP 壳复用 + 上机调试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

1. 选型:为什么是 nomacs

在这里插入图片描述

1.1 候选清单回顾

选择 nomacs —— 它是清单中**一款"原生 Qt5、用户感知强、依赖收敛于单点(exiv2)"**的候选。

1.2 nomacs 项目侧写

在这里插入图片描述

通过 GitHub API 拿到的关键事实:

  • 主页:https://github.com/nomacs/nomacs
  • 协议:GPL v3
  • 2969 stars(在 Linux 看图器赛道里头部)
  • 实现语言:C++(96%)+ CMake + 少量 Python/Shell
  • 工程根:ImageLounge/(不是仓库根)
  • 模块切分:src/DkCore/(核心:解码、缓存、元数据)+ src/DkGui/(界面:窗口、视图、菜单)
  • 源码量:46 个 .cpp + 48 个 .h,共 4.3 MB
  • 顶层 CMakeLists.txt 344 行

为什么不选最新 3.22.1 而是 3.17.2295?因为 3.17 系列是 nomacs 最后一代严格围绕 Qt5 设计的版本;3.19+ 开始往 Qt6 偏移,对 Qt5 的兼容性逐渐变差,而我们手头的 Qt-OHOS 是 5.12.12,所以从一致性出发选 3.17 最稳。

1.3 nomacs 默认依赖矩阵

来自 nomacs 顶层 CMakeLists.txt 的 9 个 option(...) 默认值:

依赖 默认 用途 鸿蒙策略
exiv2 REQUIRED(不可选) EXIF/IPTC/XMP 元数据 🎯 stub 化
OpenCV ON RAW + 高级 TIFF 解码 OFF
libraw ON 相机 RAW(依赖 OpenCV) OFF
libtiff ON 多层 TIFF OFF
QuaZip ON 浏览 ZIP 内图片 OFF
翻译 ON 多语言 .qm OFF
plugins ON 插件框架 OFF
libheif OFF HEIF OFF(保持)
libavif OFF AVIF OFF(保持)
libjxl OFF JPEG-XL OFF(保持)

💡 第一关键发现:上游 cmake/Unix.cmakeexiv2 写成 pkg_check_modules(EXIV2 REQUIRED exiv2>=0.27) —— 即使把所有 option(...) 关到 OFF,只要走 Linux 分支,exiv2 仍然是硬性依赖。这条死路在后面"双重 stub"那一节解决。


新手必看前置步骤

从0创建项目指南,新手先看这篇:
https://blog.csdn.net/weixin_52908342/article/details/161343743

QT官方鸿蒙版开源地址:https://wiki.qt.io/Qt5.12.12_Open_Source_Release_for_HarmonyOS_zh

QT官方文档地址:https://wiki.qt.io/Qt_for_OpenHarmony/zh

环境要求
(HarmonyOS/OpenHarmony)鸿蒙版本 API20+
Qt Creator安装 安装电脑版Qt5.12或以上版本(5.14、5.15),获得QtCreator的IDE。
华为 DevEco Studio 安装 如果您想开发Qt for HarmonyOS应用程序,除了使用Qt Creator之外,还需要依赖DevEco Studio。

准备 DevEco HAP 壳工程

这一步在 DevEco Studio 里做。

创建工程

在 DevEco Studio 中:

File
  New
    Create Project

选择一个最简单的 Stage 模板。工程名可以叫:

QtOhosDemo

目标结构大致类似:

QtOhosDemo/
  entry/
    src/main/
      ets/
      cpp/
    libs/

如果你使用的是 Qt for HarmonyOS 官方模板,里面通常已经有加载 Qt runtime 的代码。

设置加载库名

找到类似文件:

entry/src/main/ets/common/QtAppConstants.ets

设置:

export const APP_LIBRARY_NAME = 'libqt_ohos_demo.so';

如果你的模板没有这个文件,就搜索:

APP_LIBRARY_NAME
loadLibrary
libqohos

目标是让 HAP 启动时加载:

libqt_ohos_demo.so

放入动态库

创建目录:

entry/libs/arm64-v8a/

把下面文件放进去:

libqt_ohos_demo.so
libqohos.so
libQt6Core.so 或 libQt5Core.so
libQt6Gui.so 或 libQt5Gui.so
libQt6Widgets.so 或 libQt5Widgets.so

libqt_ohos_demo.so 来自:

~/qt-ohos-demo/build-ohos/libqt_ohos_demo.so

Qt runtime 的 .so 来自你的 Qt for HarmonyOS 安装目录。

签名 .so

在构建主机上签名:

export SIGN_TOOL=$OHOS_SDK_ROOT/toolchains/lib/binary-sign-tool

cd /path/to/QtOhosDemo/entry/libs/arm64-v8a

$SIGN_TOOL sign -inFile libqt_ohos_demo.so -outFile libqt_ohos_demo.so -selfSign 1

如果你自己拷贝了其他三方库 .so,也一起签名。

运行

在 DevEco Studio 中:

Sync Project
Build Hap
Run

成功标志:

鸿蒙 PC 上出现一个窗口,显示 Hello Qt on HarmonyOS PC

在这里插入图片描述

如果这里成功,说明:

Qt runtime 可以加载
HAP 壳工程可以运行
你的业务 .so 可以被鸿蒙应用加载

到这里就可以开始本章的nomacs适配工作了。

2. 阶段 1:环境搭建

从0创建项目指南,新手先看这篇:
https://blog.csdn.net/weixin_52908342/article/details/161343743
在这里插入图片描述

三个关键点(前几篇已验证,本次直接复用):

  1. $QT_OHOS_ROOT 指向 Qt-OHOS 5.12.12 交叉版;
  2. $OHOS_SDK_ROOT 指向官方 OHOS NDK,llvm/bin/aarch64-unknown-linux-ohos-clang 即交叉编译器;
  3. scripts/ohos-toolchain.cmake(43 行)从 NitroShare 项目原样拷贝。
# 只做一件事:把 NitroShare 那条 toolchain 拷过来
cp NitroShareGOGO/scripts/ohos-toolchain.cmake \
   nomacsGOGO/scripts/ohos-toolchain.cmake
scp nomacsGOGO/scripts/ohos-toolchain.cmake root@SERVER:/root/nomacsGOGO/

3. 阶段 2:源码下载与项目侦察

在这里插入图片描述

mkdir -p /root/nomacsGOGO && cd /root/nomacsGOGO
wget -q 'https://github.com/nomacs/nomacs/archive/refs/tags/3.17.2295.tar.gz'
tar -xzf 3.17.2295.tar.gz
cd nomacs-3.17.2295/ImageLounge

注意 nomacs 的 工程根是 ImageLounge/,不是仓库根 —— 在仓库根跑 cmake 会找不到 CMakeLists.txt。这是个反直觉的小坑。

源码切分如下:

nomacs-3.17.2295/ImageLounge/
├── 3rdparty/
│   ├── drif/             # ★ Apple ProRAW 解码头(drif_image.h)
│   └── libqpsd/          # ★ PSD 格式解码器(必须随主 .so 编进来!)
├── cmake/
│   ├── Unix.cmake        # ★ Linux 依赖检测(这里写死了 exiv2 REQUIRED)
│   └── UnixBuildTarget.cmake  # Linux 链接规则
├── src/
│   ├── DkCore/   # 模型层:DkBasicLoader / DkImageLoader / DkMetaData ...
│   ├── DkGui/    # 视图层:DkNoMacs / DkViewPort / DkWidgets ...
│   ├── main.cpp
│   └── nomacs.qrc
└── CMakeLists.txt        # 344 行顶层

4. 阶段 3:改造方案设计 —— 双重 stub 是核心

4.1 exiv2 调用面定位

在这里插入图片描述

# 谁还在引用 exiv2 命名空间?
grep -l 'Exiv2::' src/DkCore/*.cpp src/DkGui/*.cpp
# 结果:只有 src/DkCore/DkMetaData.cpp

# 谁 #include <exiv2/...>?
grep -l '#include.*exiv2' src/DkCore/*.h src/DkGui/*.h
# 结果:只有 src/DkCore/DkMetaData.h

# 调用次数
grep -c 'Exiv2::' src/DkCore/DkMetaData.cpp \
                  src/DkCore/DkBasicLoader.cpp \
                  src/DkCore/DkImageLoader.cpp
# DkMetaData.cpp:108
# DkBasicLoader.cpp:0
# DkImageLoader.cpp:0

💡 第二关键发现:exiv2 的所有调用 集中在唯一的文件 DkMetaData.cpp(1985 行)—— 这意味着可以对这一个文件实施"整文件替换",不会污染其他模块。

4.2 改造方案:双重 stub

上游 鸿蒙版改造 行数变化
CMakeLists.txt(顶层 344 行,包含 Win/Mac/Linux 多分支 + plugins + 翻译) 重写为精简版 344 → 123
cmake/Unix.cmake + cmake/UnixBuildTarget.cmake(exiv2 REQUIRED + 多 target) 不调用,绕开
src/DkCore/DkMetaData.cpp(1985 行 exiv2 重度调用) 整体替换为空 stub 1985 → 173
src/DkCore/DkMetaData.h + <exiv2/*.hpp>(编译期类型依赖) 提供 7 个 stub 头 0 → 9 KB stub 头

4.3 产物形态

上游 鸿蒙版
add_executable(nomacs ...) + add_library(nomacsCore SHARED ...)(2 个 target) 合并 成单一 add_library(nomacs SHARED ...),导出 T main 符号
默认带 SONAME = libnomacs.so.3 NO_SONAME TRUE(鸿蒙 dlopen 只识别裸名)

5. 阶段 4:CMake 顶层重写 + exiv2 stub 头

5.1 全新精简版 CMakeLists.txt

把原 344 行的顶层 CMakeLists.txt 整体替换为以下精简版(关键节选):

cmake_minimum_required(VERSION 3.10)
project(nomacs CXX)

# ---- 全部可选依赖关闭 ----
option(ENABLE_OPENCV       "" OFF)
option(ENABLE_RAW          "" OFF)
option(ENABLE_TIFF         "" OFF)
option(ENABLE_QUAZIP       "" OFF)
option(ENABLE_TRANSLATIONS "" OFF)
option(ENABLE_PLUGINS      "" OFF)
option(ENABLE_HEIF         "" OFF)
option(ENABLE_AVIF         "" OFF)
option(ENABLE_JXL          "" OFF)

# ---- ★ exiv2 stub include 优先:让 stub 头胜出于真实 exiv2 头 ----
include_directories(BEFORE /root/nomacsGOGO/exiv2-stub)

find_package(Qt5 5.12 REQUIRED COMPONENTS
    Core Gui Widgets Network PrintSupport Concurrent Svg)

# ---- 源码搜集 ----
file(GLOB GUI_SOURCES  "src/DkGui/*.cpp")
file(GLOB CORE_SOURCES "src/DkCore/*.cpp")
list(REMOVE_ITEM GUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/DkPluginManager.cpp")

# ---- ★ 3rdparty/libqpsd 必须随主 .so 编进来,否则 dlopen 会报:
#       Error relocating libnomacs.so: _ZN11QPsdHandlerC1Ev: symbol not found
set(LIBQPSD_SOURCES
    ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libqpsd/qpsdhandler.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libqpsd/qpsdhandler_p.cpp)

include_directories(
    ${CMAKE_BINARY_DIR}
    ${CMAKE_CURRENT_SOURCE_DIR}/src
    ${CMAKE_CURRENT_SOURCE_DIR}/src/DkCore
    ${CMAKE_CURRENT_SOURCE_DIR}/src/DkGui
    ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libqpsd
    ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/drif      # ★ drif_image.h
)

qt5_add_resources(NOMACS_RCC src/nomacs.qrc)

# ---- ★ 关键:合并 binary + DkCore 成单一 SHARED 库,并导出 main ----
add_library(nomacs SHARED
    ${NOMACS_EXE_SOURCES} ${GUI_SOURCES} ${CORE_SOURCES}
    ${LIBQPSD_SOURCES}
    ${NOMACS_RCC})

set_target_properties(nomacs PROPERTIES
    OUTPUT_NAME "nomacs"
    PREFIX      "lib"
    NO_SONAME   TRUE)        # ★ 不要 libnomacs.so.3 这种带版本号的 SONAME

target_compile_definitions(nomacs PRIVATE
    DK_CORE_DLL_EXPORT
    DK_DLL_EXPORT
    NOMINMAX)

target_link_libraries(nomacs
    Qt5::Widgets Qt5::Gui Qt5::Network
    Qt5::PrintSupport Qt5::Concurrent Qt5::Svg)

💡 关键设计点

  1. 不调用 cmake/Unix.cmake:那里把 exiv2 写死成 REQUIRED;
  2. include_directories(BEFORE /root/nomacsGOGO/exiv2-stub):让 stub 头优先于真实 exiv2 被命中;
  3. add_library(nomacs SHARED) 而不是 add_executable:鸿蒙 HAP 的 Qt 加载器只 dlopen 一个 .so 然后 dlsym("main") 调用入口;
  4. NO_SONAME TRUE:避免生成带版本号的 SONAME;
  5. LIBQPSD_SOURCES 必须显式列入 —— 这是后面踩坑 1 的根因,第一次写 CMake 时只加了 include 路径忘了源码,链接通过但 dlopen 时炸。

5.2 exiv2 stub 头

在这里插入图片描述

scripts/exiv2-stub/exiv2/exiv2.hpp 提供 nomacs 用到的 Exiv2 名字的最小定义(节选):

namespace Exiv2
{
typedef unsigned char byte;

class Exifdatum {
public:
    std::string key()       const { return {}; }
    std::string toString()  const { return {}; }
    long_t toLong(long = 0) const { return 0; }
    Exifdatum &operator=(const std::string &) { return *this; }
    int setValue(const std::string &) { return 0; }
};
class Xmpdatum  { /* ... 同样的空实现 ... */ };
class Iptcdatum { /* ... 同样的空实现 ... */ };

template <typename Datum> class DataIterator {
public:
    Datum &operator*()                       { static Datum d; return d; }
    DataIterator &operator++()               { return *this; }
    bool operator!=(const DataIterator &) const { return false; }
};

class ExifData {
public:
    typedef DataIterator<Exifdatum> iterator;
    iterator begin() { return {}; }
    iterator end()   { return {}; }
    bool empty() const { return true; }
    Exifdatum &operator[](const std::string &) { static Exifdatum d; return d; }
    void add(const Exifdatum &) {}
};
class XmpData  { /* ... */ };
class IptcData { /* ... */ };

class Image {
public:
    typedef std::unique_ptr<Image> UniquePtr;
    virtual void readMetadata()  {}
    virtual void writeMetadata() {}
    ExifData &exifData() { return mExifData; }
    XmpData  &xmpData()  { return mXmpData;  }
    IptcData &iptcData() { return mIptcData; }
private:
    ExifData mExifData;
    XmpData  mXmpData;
    IptcData mIptcData;
};

class ImageFactory {
public:
    static Image::UniquePtr open(const std::string &)
    { return Image::UniquePtr(new Image); }
    static Image::UniquePtr open(const byte *, long)
    { return Image::UniquePtr(new Image); }
};
} // namespace Exiv2

加上 6 个重定向头(image.hpp / preview.hpp / xmpsidecar.hpp 等),它们都只有两行:

#pragma once
#include <exiv2/exiv2.hpp>

总共 9 KB 的 stub 头就能让 DkMetaData.h 的所有 #include <exiv2/...> 编过。

5.3 DkMetaData.cpp 整体 stub(173 行替换原 1985 行)

#include "DkMetaData.h"
#include <QImage>
#include <QSize>
#include <QStringList>
#include <QVector2D>

namespace nmc
{
DkMetaDataT::DkMetaDataT() = default;

bool DkMetaDataT::isNull()                       { return true; }
QSharedPointer<DkMetaDataT> DkMetaDataT::copy() const
{ return QSharedPointer<DkMetaDataT>(new DkMetaDataT); }

void DkMetaDataT::readMetaData(const QString &filePath, QSharedPointer<QByteArray>)
{
    mFilePath = filePath;
    mExifState = no_data;       // 关键:永远停在 no_data,让上层走"无元数据"分支
}

bool   DkMetaDataT::saveMetaData(const QString &, bool)            { return false; }
QString DkMetaDataT::getDescription()                          const { return {}; }
int     DkMetaDataT::getOrientationDegree()                    const { return 0; }
QImage  DkMetaDataT::getThumbnail()                            const { return {}; }
QImage  DkMetaDataT::getPreviewImage(int)                      const { return {}; }
bool    DkMetaDataT::hasMetaData()                             const { return false; }
/* ... 其余 60 多个 isXxx / getXxx / setXxx 全部返回默认值 ... */

QString DkMetaDataT::exiv2ToQString(std::string s) {
    return QString::fromStdString(s);    // 唯一保留的真实逻辑(不依赖 exiv2)
}

void DkMetaDataT::setQtValues(const QImage &cImg)
{
    mQtKeys.clear(); mQtValues.clear();
    if (!cImg.isNull()) {
        mQtKeys   << "Width" << "Height";
        mQtValues << QString::number(cImg.width())
                  << QString::number(cImg.height());
    }       // ← Width / Height 这种 Qt 自身就能算的值,给真实结果
}

// loadSidecar / setXMPValue 这些必须返回 Exiv2 类型的方法 —— 用 stub 类构造空对象
std::unique_ptr<Exiv2::Image> DkMetaDataT::loadSidecar(const QString &) const
{ return std::unique_ptr<Exiv2::Image>(new Exiv2::Image); }
bool DkMetaDataT::setXMPValue(Exiv2::XmpData &, QString, QString) { return false; }

// DkMetaDataHelper 的所有方法也是空实现
void DkMetaDataHelper::init() { /* clear all lists */ }
void DkMetaDataHelper::initialize() { getInstance().init(); }
QString DkMetaDataHelper::translateKey(const QString &key) const { return key; }
/* ... 其余全部返回空 ... */
} // namespace nmc

💡 关键设计点

  • DkMetaData 的所有方法签名 1:1 保留 —— 这样 DkGui 里上百处 metaData->getXxx() 调用全部不需要改;
  • 大多数返回空值 / false / 0 —— 上层会走"该图无元数据"的分支,不会崩溃;
  • 唯一保留真实逻辑的两个:exiv2ToQString(纯字符串转换)和 setQtValues(Width/Height 用 QImage 自己算)—— 这两个对"看图"主功能有真实用途。

6. 阶段 5:Qt 5.14+/5.15+ → 5.12 API 降级(5 处补丁)

在这里插入图片描述

nomacs 3.17 已经开始用 Qt 5.14+ 的新 API,而 Qt-OHOS 是 5.12.12,这里有 5 处必须降级:

6.1 三处批量替换(sed 一把搞定)

grep -rln 'Qt::SkipEmptyParts\|Qt::endl\|moveToTrash' src/ | while read f; do
    sed -i 's|Qt::SkipEmptyParts|QString::SkipEmptyParts|g' "$f"
    sed -i 's|Qt::endl|endl|g'                              "$f"
    sed -i 's|return file\.moveToTrash();|return file.remove();|g' "$f"
done
新 API 出现于 Qt 替代方案 影响
Qt::SkipEmptyParts 5.14 QString::SkipEmptyParts(旧位置) 等价
Qt::endl 5.14 全局 endl(来自 <QTextStream> 等价
QFile::moveToTrash() 5.15 QFile::remove() 降级:直接删除而不是移到回收站

6.2 两处单点改写

// src/DkGui/DkWidgets.cpp:3101
- QScreen *myScreen = screen();                       // QWidget::screen() 是 5.14+
+ QScreen *myScreen = QGuiApplication::primaryScreen();

// src/DkCore/DkBaseViewPort.cpp:498
- zoomLeveled(factor, event->position());             // QWheelEvent::position() 是 5.14+
+ zoomLeveled(factor, event->posF());

6.3 一处缺标准头

// src/DkCore/DkMath.h
  #pragma once
+ #include <ostream>      // ← 用 std::ostream<<"..."  必须显式 include
  #include <QDebug>

💡 第三关键发现std::ostream << "字符串字面量" 在 musl-libc / libc++ 组合下必须显式 #include <ostream>,否则会去匹配到 QDebug 的重载,报"invalid operands ... const char[2]"。glibc + libstdc++ 上因为传递包含经常碰巧能编过,迁到 OHOS NDK(musl + libc++)就暴露了。


7. 阶段 6:编译 + 5 项产物自检

7.1 一次配置 + 一次编译

在这里插入图片描述

cd /root/nomacsGOGO/nomacs-3.17.2295/ImageLounge
mkdir build && cd build

cmake -G 'Unix Makefiles' \
    -DCMAKE_TOOLCHAIN_FILE=/root/nomacsGOGO/ohos-toolchain.cmake \
    -DCMAKE_BUILD_TYPE=Release ..
# Configuring done (0.1s)

make -j4 2>&1 | tee build.log
# [ 60%] Building CXX object .../DkCore/DkTimer.cpp.o
# [ 62%] Building CXX object .../DkCore/DkUpdater.cpp.o
# [ 66%] Linking CXX shared library libnomacs.so
# [100%] Built target nomacs

grep -cE 'error:' build.log
# 0

7.2 5 项产物自检

在这里插入图片描述

# 期望 实际
1 file libnomacs.so ELF aarch64 shared object ✅ ELF 64-bit LSB shared object, ARM aarch64
2 size 几 MB 4.9 MB
3 llvm-nm -D 是否有 T main T main 0x21a19c T main
4 llvm-readelf -d NEEDED 仅 Qt5 + libc ✅ 只有 Qt5Network/PrintSupport/Concurrent/Svg/Widgets/Gui/Core + libc++_shared + libc,没有 libexiv2 / libopencv / libraw
5 LOAD 段对齐 0x1000 (4 KB) ✅ 4 个 LOAD 段全 0x1000

5/5 全过。


8. 阶段 7:HAP 工程构建

从0创建项目指南,HAP 工程构建先看这篇:
https://blog.csdn.net/weixin_52908342/article/details/161343743

QT官方鸿蒙版开源地址:https://wiki.qt.io/Qt5.12.12_Open_Source_Release_for_HarmonyOS_zh

QT官方文档地址:https://wiki.qt.io/Qt_for_OpenHarmony/zh

8.1 壳复用

# 1) 复制demoOhos 整壳,去掉构建产物
cp -R demoOhos nomacsOhos
rm -rf nomacsOhos/{.idea,build,oh_modules,*/build,*/oh_modules}

# 2) 替换库文件
rm  nomacsOhos/entry/libs/arm64-v8a/libnitroshare.so
cp  build/libnomacs.so  nomacsOhos/entry/libs/arm64-v8a/libnomacs.so

# 3) 5 处文案替换(grep 出来一共只有 3 个文件 6 处)
# 文件 1:AppScope/resources/base/element/string.json   "value": "nomacs"
# 文件 2:entry/.../element/string.json                 module_desc / QAbility_*
# 文件 3:entry/.../common/QtAppConstants.ets           APP_LIBRARY_NAME = 'libnomacs.so'
#                                                       LOG_TAG          = 'nomacsOhos'

在这里插入图片描述

8.2 4 项 HAP 自检

# 结果
1 APP_LIBRARY_NAME 指向 libnomacs.so
2 entry/libs/arm64-v8a/libnomacs.so 就位 ✅ 4.9 MB
3 bundleName com.example.gloggohos沿用 GloggOhos 签名证书,免重新申请,可直接 ▶ Run
4 桌面显示名 / module 文案 ✅ 全部 nomacs

完成后,DevEco Studio 打开 nomacsOhos/,▶ Run,鸿蒙 PC 桌面就出现一个 nomacs 图标。


在这里插入图片描述

9. 阶段 8:上机调试 —— 两个 dlopen 期 symbol-not-found 坑

第一次部署到华为 MateBook Pro 上,启动 1 秒就 SIGABRT 闪退。看 LastFatalMessage

LastFatalMessage:
  dlopen() failed to open library '/data/storage/el1/bundle/libs/arm64/libnomacs.so':
  Error relocating libnomacs.so: _ZN11QPsdHandlerC1Ev: symbol not found

9.1 坑 1:_ZN11QPsdHandlerC1Ev: symbol not found

反混淆QPsdHandler::QPsdHandler() —— 3rdparty/libqpsd/ 里 PSD 格式解码器的构造函数。

根因:我在重写新版 CMakeLists.txt 时,只把 3rdparty/libqpsd 加进了 include 路径,但忘了把它的 .cpp 文件加进 add_library 源码列表DkBasicLoader.cpp 里有 new QPsdHandler(...) 直接引用了这个类,链接时 musl-libc 的链接器默认允许未解析符号通过(只警告不报错),运行时鸿蒙 dlopen 严格校验所有符号必须能 relocate,于是炸。

修复(在 CMakeLists.txt 里):

set(LIBQPSD_SOURCES
    ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libqpsd/qpsdhandler.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libqpsd/qpsdhandler_p.cpp)

add_library(nomacs SHARED
    ...
    ${LIBQPSD_SOURCES}      # ← 把它显式加进来
    ${NOMACS_RCC})

注意不要带 qpsdplugin.cpp —— 那是 Qt image plugin 的入口,独立 .so 才需要,主程序里加进来反而会污染 plugin 注册系统。

9.2 坑 2:qt_resourceFeatureZlib: symbol not found

修完坑 1 重新部署,第二次 SIGABRT

LastFatalMessage:
  dlopen() failed to open library '/data/storage/el1/bundle/libs/arm64/libnomacs.so':
  Error relocating libnomacs.so: qt_resourceFeatureZlib: symbol not found

根因:服务器上的 host rcc-qt5 是 Qt 5.15.11 版本(系统 yum 包),它在生成 qrc_nomacs.cpp 时引用了 Qt 5.15+ 才有的 qt_resourceFeatureZlib 符号;但 target 是 Qt-OHOS 5.12.12,没这个符号 —— 经典的 host/target Qt 版本错配。

[root]# which rcc-qt5
/usr/bin/rcc-qt5
[root]# rcc-qt5 --version
rcc-qt5 5.15.11                  # ← 来自 host 系统包

修复:用 rcc --no-compress 重新生成 qrc_nomacs.cpp,避开 zlib 特性:

cd build
rcc-qt5 --no-compress --name nomacs ../src/nomacs.qrc -o qrc_nomacs.cpp
# 旧 cpp:17341 行,2 处 qt_resourceFeatureZlib
# 新 cpp:17997 行,0 处 qt_resourceFeatureZlib   ✅

rm CMakeFiles/nomacs.dir/qrc_nomacs.cpp.o
make -j4         # 重新 link

--no-compress 让资源数据按字面量展开(cpp 多 656 行),代价就是 .so 大几 KB,但运行时不再需要 zlib 解压。

9.3 防御性自检:不再有第三个坑了

修完两个坑后,我写了一个 check_missing_symbols.sh 脚本主动扫所有 _qt* / qt_* / Q_* / _q_* 开头的 undefined 符号,确认这类 host/target 错配的隐患全部清零

=== nomacs.so 中所有 "qt_*" / "_qt*" / "Q_*" / "_q_*" 开头的 undef 符号 ===
(empty — 0 hits)

=== 全部 zlib / inflate / deflate / compress 相关 undef ===
_ZN12QImageWriter14setCompressionEi    # 这个是 QImageWriter::setCompression,
                                       # 由 libQt5Gui 提供,不是 zlib,正常

第三次部署,应用启动成功,nomacs 主窗口正常显示,可浏览图片。🎉


在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

10. 总结:本次移植的 4 个独立看点

回顾本系列前 5 篇(KDiff3 / DiffPDF / glogg / QElectroTech / NitroShare)和本篇 nomacs,nomacs 这一篇的"独门技巧"集中在 4 点:

10.1 双重 stub:解决"项目深度依赖一个无法剥离的 C++ 库"

之前几篇遇到的依赖都是相对好处理的:

  • glogg:boost::program_options → 用 QCommandLineParser 替换(功能等价);
  • NitroShare:openssl → 直接 dlopen 失败时降级(功能裁剪);

nomacs 的 exiv2 不一样:它的 100+ 处调用嵌入了核心数据流(DkMetaData),且类型系统(Exiv2::Image / Exiv2::XmpData)出现在头文件的 public/protected 接口中

解法是首次实战的"双重 stub":

  • stub 头(9 KB,7 个文件)让 #include <exiv2/...> 编过;
  • stub cpp(173 行,替换原 1985 行)让所有方法变成空实现;
  • 调用点(DkGui、DkBasicLoader 等)一行不改

10.2 Qt 5.14+/5.15+ → 5.12 降级清单

第一份"完整可复用的 Qt 新版 API → 5.12 降级清单":

新 API Qt 降级方案
Qt::SkipEmptyParts 5.14 QString::SkipEmptyParts
Qt::endl 5.14 全局 endl
QFile::moveToTrash() 5.15 QFile::remove()
QWidget::screen() 5.14 QGuiApplication::primaryScreen()
QWheelEvent::position() 5.14 QWheelEvent::posF()

未来移植任何用 Qt 5.14+ 写的项目到 Qt-OHOS 5.12,这张表都可以一键贴上。

10.3 musl-libc + libc++ 组合下的"伪传递包含"陷阱

std::ostream << "字符串" 在 GNU 工具链(glibc + libstdc++)下经常因为别的头偷偷拉进 <ostream> 而碰巧编过;切到 OHOS NDK 的 musl-libc + libc++ 组合就立刻暴露 —— 它的标准头不会偷传递,必须显式 #include

这条规则未来可以快速辅助定位只在鸿蒙交叉编译失败、本机能编的诡异错误。

10.4 “链接通过 ≠ 运行通过”——主动扫 undefined 符号

本次最痛的两个坑(QPsdHandlerqt_resourceFeatureZlib)共同特征是:

  • 链接阶段全部通过(musl 链接器默认 lazy/弱解析)
  • dlopen 阶段才 SIGABRT(鸿蒙严格校验所有符号必须可 relocate)

经验:鸿蒙 PC 移植在 link 成功后必须再跑一次 llvm-nm -u 扫 undefined 符号,确认没有形如 qt_* / _q_* 这类引用,否则上机必崩。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐