
1. 项目背景与核心痛点1.1 license_checker 在开源合规审计里的价值先把结论放在前面License 审计这事在鸿蒙生态里比在安卓时代更麻烦也更躲不掉。一个 Flutter 应用跑起来之后背后到底依赖了多少个开源项目如果你从来没系统性查过多半会被数字吓一跳。我手上这个不算大的应用只算 pub.dev 上的直接依赖就有 60 多个再算上它们的传递依赖、原生带进来的开源 C/C 库轻轻松松破百。过去很多团队对这件事的态度是“只要能编译过就不管”但当你把应用提交到应用市场、尤其是有政企采购或出海需求的时候开源协议审计几乎是一个绕不开的硬指标审核方问你要 license 清单的频率远比你想象中高。license_checker 在 Flutter 生态里干的事情很聚焦读取项目锁文件和包配置文件把每个依赖的名字、版本、许可证类型、版权声明汇总出来再生成一份结构化的开源声明。它解决的不只是“合规检查”这个看似法务的问题更重要的是避免了人工维护声明文件的灾难——你没法保证每次升级依赖都记得去更新一条记录但机器可以。我在项目里遇到过最典型的一次事故是把某个图表库从 1.0 升到 2.0顺手把依赖里的一个底层压缩库从 MIT 协议换成了 LGPL 协议声明文件没同步更新结果审核环节被卡了两天最后只能人工去翻 changelog 补材料。如果当时就把 license_checker 作为构建环节的一部分这种问题根本不会发生。1.2 原库在普通平台上的运行机制要把一个库挪到鸿蒙上第一步不是找鸿蒙的 API 文档而是先吃透它原来是怎么写的。license_checker 的核心逻辑并不复杂大致分三步。第一步是建立依赖清单。它通过解析pubspec.lock和package_config.json拿到当前项目实际使用的所有包。注意pubspec.lock锁定的是精确版本package_config.json则记录了每个包在磁盘上的真实映射路径这两份文件共同构成依赖发现的基础。单独读 lock 文件能知道版本号但拿不到包的具体位置所以在很多实现里两者都要读。第二步是识别许可证。每个 Flutter 包安装时通常会带LICENSE或LICENSE.txt文件库会读取这些文本再和内置的许可证模板做比较。比较并不是简单的字符串相等而是要做大小写归一化、空白归一化否则版权声明里的格式化差异会导致识别失败。内置模板基本覆盖 MIT、Apache-2.0、BSD-3-Clause 这些主流协议但遇到一些冷门协议或者自定义许可证时就很容易走到“无法识别”的分支。第三步是报告生成和展示。库提供两套出口一套是命令行工具可以把结果导出成 Markdown、HTML 或 JSON 文件另一套是应用内展示页面用户可以在“关于”里查看完整的开源声明。对于大多数团队前者是给审计和交付用的后者是给终端用户看的。这套机制在 Android、iOS、macOS 等常规平台都很成熟开发者只需要在pubspec.yaml里加上依赖调用一个 API 就能拿到列表。但问题是它隐含预设了一个条件dart:io可以随意读取当前工作目录或项目根目录的文件资源加载也可以依赖rootBundle。这个假设在一套新操作系统上往往不成立尤其是鸿蒙的沙箱体系下。1.3 鸿蒙平台上暴露出的三类主要问题我们的应用要上架鸿蒙HarmonyOS NEXT把原有 Flutter 工程迁移到鸿蒙专有引擎上编译。跑起来之后license_checker 直接原地爆炸问题集中在三方面。第一是文件路径失效。原库在解析依赖时用了大量绝对路径拼接比如File(rootPath /pubspec.lock)但鸿蒙应用沙箱里的运行目录和开发机不一致导致pubspec.lock根本找不到。更麻烦的是哪怕找到了pubspec.lock里面的包路径仍然是开发机上的绝对路径直接拿过去解析就会读到不存在的目录。这个问题看日志时最迷惑因为异常信息只是一个普通的FileSystemException完全不提是什么原因导致的。第二是平台通道缺失。原库的导出功能在移动端通常会借助系统的文件分享面板这部分底层走的是 Flutter 的 MethodChannel 调用原生代码。鸿蒙侧如果没有插件提供对应的平台实现一调用就会收到MissingPluginException。这类异常不像空指针那么好排查因为它在 Dart 层往往表现为某个 Future 永远无法完成或者直接抛错后才浮现出真面目。第三是资源打包路径不统一。在 Android 上Flutter 资源被打进 APK 的 assets 目录rootBundle能稳定读取鸿蒙上资源归档到 HAP 包内路径前缀和读取方式与 Android 存在差异。license_checker 某些版本会内置一些辅助数据文件加载行为不完全一致需要我们做兼容处理。这三个问题不是孤立的改一个地方往往会牵出另外两个。所以动手之前我先把整个适配方案理了一遍而不是急着改代码。2. 鸿蒙适配的整体规划与技术选型2.1 适配目标与改造边界拿到一个三方库要跨平台移植第一件事不是改代码而是划定改造边界。我们的目标是在鸿蒙平台上license_checker 达到和 Android 相同的可用性。具体拆成四个小目标能扫描出鸿蒙工程中 Flutter 依赖的完整清单能识别出每个依赖对应的开源许可证能生成 Markdown 和 JSON 两种格式的声明文件能提供应用内展示和导出能力。其中前三个是必须的第四个可以根据鸿蒙生态成熟度打折。我们的原则很简单能用纯 Dart 解决的就用纯 Dart解决不了的才去桥接 ArkTS 原生层。这个原则听起来很理所当然但实际操作中很多人会走反一开始就试图去鸿蒙工程里实现各种原生插件结果越做越复杂。实际上 license_checker 的核心能力百分之九十都可以在 Dart 生态内完成真正需要碰原生层的只有文件分享这类交互功能而那部分可以降级处理。2.2 平台识别与代码隔离方案license_checker 原本只识别 Android、iOS、macOS 等常规平台整个代码里没有保留鸿蒙的分支也不能直接加一个Platform.isOhos因为鸿蒙适配版 Flutter 引擎没有暴露这个常量。我们采用编译期标记的方式const bool isOhos bool.fromEnvironment(OHOS, defaultValue: false);然后把所有平台相关逻辑收拢到一个独立文件platform_adapter.dart里内部根据isOhos选择不同的实现。这样能确保原有平台不受影响也方便后续抽成独立插件。选择这条路而不是大量加if (Platform.isLinux)这类运行时判断是考虑到两点。一是鸿蒙的 Flutter 适配版还没有完全对齐上游运行时嗅探系统特性容易拿到错误信息而且误判概率比预想的高二是编译期变量在发布时就被编译器树摇掉不会产生任何运行时开销也不会在日志里暴露平台差异。2.3 依赖管理策略fork 一份私有维护分支改造后的代码不能直接发布到 pub.dev 上作为正式版本因为它依赖了鸿蒙专有的编译参数放在公开源上会让其他平台用户莫名其妙。我们的做法是 fork 一份仓库在独立分支上维护鸿蒙适配然后通过 Git 依赖串进工程dependencies: license_checker: git: url: https://gitee.com/your-team/license_checker.git ref: ohos-support使用 Git 依赖唯一要注意的是版本锁定。Git 依赖不会自动遵守语义化版本同一个 ref 如果被强制推送更新团队里其他人拉到的代码可能就不一样了。我们的做法是固定 commit 而不是分支名改代码时主动更新 commit 并同步到工程配置里。这样虽然多一步操作但稳定性大幅提升。3. 核心改造流程与实操要点3.1 第一步重构依赖发现模块原库的入口是LicenseChecker类构造时会接收一个路径参数。改造前它默认读取调用方传入的绝对路径鸿蒙下我们不能信任这个路径改为优先从包的package_config.json中提取rootUri来定位工程根目录。这里有一个很关键的细节package_config.json里的 rootUri 是相对路径需要以自己的所在目录为基准再做一次拼接。原库解析时直接用 Dart 的Uri解析鸿蒙适配版引擎对相对 URI 的处理与桌面平台有细微差异所以我们干脆改成了手动路径拼接避免在不同实现上踩坑。代码层面我重写了_resolveProjectRootFutureString _resolveProjectRoot() async { if (isOhos) { final config await _findPackageConfig(); if (config ! null) { final uri config.uri.resolve(.).toFilePath(); if (_dirExists(uri)) return uri; } // 兜底从当前可执行文件目录往上找 pubspec.yaml return _searchUpwardForPubspec(Directory.current.path); } // 原有逻辑保持不变 return _legacyProjectRoot(); }这段代码里最值得注意的就是_searchUpwardForPubspec这个兜底函数。鸿蒙的沙箱目录层级很深直接使用Directory.current往往指向应用的可执行目录而不是工程目录所以需要向上搜索pubspec.yaml找到第一个包含该文件的目录就停下来。这个逻辑让同一个工具既能服务开发期扫描也能服务运行期展示两种场景下都稳定。3.2 第二步许可证识别的兼容处理许可证识别部分总体稳定但我们做了三处增强全部是为鸿蒙场景准备的。第一处是增加了缺失 LICENSE 文件的兜底。部分鸿蒙适配的 Flutter 插件打包后LICENSE 文件没有被打进 asset导致读取为空。我们增加了远程仓库兜底根据包名和版本从 pub.dev 的 API 拉取 metadata匹配许可证字段。如果 pub.dev 也没有记录就标记为“待人工确认”而不是直接跳过。这样虽然不能自动完成全部审计但至少不会让问题隐藏在流程之外。第二处是统一文本归一化规则。原库计算的相似度阈值是 0.8在鸿蒙侧遇到部分中文本地化修改过的许可证文本时误判率明显上升。我调整了算法先去 BOM 和所有不可见字符再做小写化和关键词权重比对。MIT 和 BSD-3-Clause 这类结构相似的协议重点区分版权声明占位符降低了“识别成了但实际不对”的概率。第三处是缓存位置的调整。识别结果需要缓存起来避免每次启动都重新读取和比对几十个包的 LICENSE 文件。原库缓存到临时目录但鸿蒙沙箱对临时目录有清理策略应用随时可能被系统回收缓存文件所以我们把缓存放到了应用支持目录。实测下来配合path_provider的鸿蒙适配版二次扫描的速度基本在 200ms 以内体感和 Android 没有差别。3.3 第三步声明文件生成的模板与输出声明文件是审计工作的最终产物格式直接影响后续的法务和审核效率。原库的 Markdown 模板相对简单我们基于鸿蒙上架要求做了增强每个依赖输出为一个表格行依赖名版本许可证版权信息来源地址flutter_secure_storage9.2.2MITCopyright (c) 2019 German Saprykinhttps://github.com/mogol/flutter_secure_storage生成逻辑依然是纯 Dart 实现模板单独放在assets/license_template.tmpl文件里用字符串替换的方式填充数据不依赖任何模板引擎。这样做的好处是减少依赖数量避免把更多第三方库带进鸿蒙适配范围坏处是模板语法非常原始一旦要加复杂逻辑就得在代码里拼字符串。目前对我们来说足够用。JSON 输出也保留了这是给程序用的。CI 脚本在比对声明文件时只需要解析 JSON 判断依赖集合和许可证类型是否发生变化即可不需要解析 Markdown 表格。3.4 第四步应用内展示与导出交互应用内展示这部分原库提供showLicensePage风格的页面本质上就是根据解析结果渲染一个 ListView。鸿蒙适配版 Flutter 对基础控件渲染的支持足够这部分改动很小只需要在页面初始化时传入我们改造过的数据源即可。真正的坑在导出按钮。Android 上的分享面板依赖原生实现鸿蒙上没有现成的插件可以直接调。我们最终采用了降级方案点击导出时把声明文件保存到应用文档目录同时弹出一个对话框告诉用户文件的绝对路径并额外提供“复制到剪贴板”的按钮。这个方案不惊艳但完全规避了平台通道带来的不确定性用户也能通过系统的文件管理访问文档目录。如果你非要保留原生的导出面板体验也不是不能做需要在鸿蒙工程里自己注册一个 MethodChannel接收 Dart 侧传过来的文件路径然后调用系统的文件分享能力。这个工作量不小而且需要同时维护 Dart 和 ArkTS 两边的代码收益却有限我不建议一开始就投入进去。3.5 上架前最后一步把声明文件放进 HAP鸿蒙的 HAP 打包和 Android 的 APK 有区别静态声明文件不应该只放在 Flutter asset 里因为有的审核流程会要求它出现在工程原始资源中。我们最终把生成的声明文件放进ohos/app/src/main/resources/rawfile目录这样它会被完整打进 HAP业务代码可以随时通过资源管理器读取。这一步如果漏了最直接的表现就是审核人员反馈说“找不到开源声明文件”而你本地明明已经生成过了。由于打包产物在真机上的路径不容易直观查看这个问题排查起来特别费劲建议在 CI 流程里加一个专门的归档步骤把声明文件和 HAP 放在一起交付避免靠人肉确认。4. 常见问题与排查技巧实录4.1 MissingPluginException平台通道没有一个实现这个异常在鸿蒙移植初期出现的频率最高。很多人一看到异常就到处搜鸿蒙的 MethodChannel 文档其实最有效的步骤是先判断这个调用到底是不是必需的。我们的经验是能绕开的先用纯 Dart 绕开。比如之前提到的分享导出功能改成本地保存加剪贴板三行代码解决问题比去实现一个新的 ArkTS 方法快得多。如果确实需要调用原生能力比如读取系统信息那就要检查插件的ohos目录下是否真的有原生代码。常见的坑是插件在 pub.dev 上声称支持鸿蒙实际上只是把 Android 代码原样放在仓库里没有对应的鸿蒙实现。这种情况下再怎么配置依赖都无济于事只能换方案或者自己补。4.2 Directory listing failed沙箱目录读取权限不足我们在真机上遇到过一种情况Directory.current明明存在但调用list()时返回权限错误。原因是鸿蒙应用默认沙箱的可读范围是受限的不是所有目录都能随便列目录、读文件。解决办法是不要依赖当前目录而是通过getApplicationSupportDirectory()这类能拿到真实沙箱路径的 API。另一个相关细节是如果应用确实需要读取公共存储目录下的文件必须在module.json5里声明对应权限并且运行时动态申请用户授权。license_checker 本身不需要在运行时读取公共目录所以我们在适配中直接不碰这些权限减少审核风险。4.3 pubspec.lock 与 package_config.json 不一致多平台开发时一个工程可能同时维护 Android 和鸿蒙两套构建配置。我们出现过pubspec.lock里记录的版本和package_config.json指向的包不一致导致许可证识别结果混乱。排查下来发现是团队有人直接改了 pubspec.yaml 但没有重新执行flutter pub get锁文件暂时是旧内容。这类问题靠代码很难兜住最终我们在 CI 里加了一步校验每次扫描前先对比两份文件的依赖集合如果差异超过一定阈值就直接报错提醒开发者执行flutter pub get。这个校验逻辑写起来很简单但真的能省下不少看日志的时间。鸿蒙适配版 Flutter 引擎对锁文件的洁癖程度比官方版更高一旦不一致报错信息还经常是无关紧要的警告容易误导排查方向。4.4 中文内容乱码与模板转义另一个看起来不严重但实际很折腾的问题是乱码。鸿蒙的编译工具链在部分环境下默认的字符集不是 UTF-8导致生成的声明文件里中文字段变成一串问号。排查到最后发现不是代码的问题而是 CI 的工作流里没有设置环境变量。解决办法很朴素在生成文件的写入代码里强制指定utf8编码同时确保传入的字符串本身是标准 Dart String不要夹杂字节流。下面这个写法是安全的final content _renderLicenseMarkdown(); await File(path).writeAsString(content, encoding: utf8);如果是在 Windows 上要额外注意终端脚本的编码设置否则同样的代码在不同机器上行为完全不同。我们用 GitHub Actions 跑 Linux 环境没有遇到问题但本地 Windows 开发者复现乱码时最终定位到 PowerShell 的默认编码不是 UTF-8。5. 自动化集成与后续扩展5.1 把审计流程挂进 CI手工跑一次 license_checker 只能算临时检查要让合规审计持续生效必须把它挂进 CI。我们用的流程是每次提交 Pull Request 时在工作流里跑一次命令行模式生成最新的声明文件然后和仓库里的旧文件比对重点关注三个维度依赖集合是否变化新增、删除、升级新增依赖是否已经通过许可证识别是否存在许可证状态为“待人工确认”的依赖。一旦有未确认项CI 会直接卡住合并按钮并把报告发到团队沟通群里。刚开始大家觉得这有点严格但第一个月就拦下了三个许可证变更体验过之后都认可了这套机制。下面是 CI 步骤里最核心的命令dart run license_checker export --format markdown --output THIRD_PARTY_LICENSES.md dart run license_checker check --strict第一条命令负责生成第二条负责校验。在--strict模式下只要有任何依赖未识别或识别不确定进程就返回非零退出码流水线自然中断。需要注意命令行模式对鸿蒙工程也有效因为命令行的运行环境和真机沙箱不同目录可访问性反而更好。5.2 与鸿蒙应用市场审核流程衔接声明文件生成好之后具体放在哪里、怎么写不同审核渠道的要求有细微差别。以我们对接的情况来说普遍要求是在应用内提供一个“开源软件声明”的入口用户点击后能看到完整的协议列表。为了稳妥我们把声明文件同时做成三个副本一个放进rawfile随 HAP 打包一个挂在应用内的“关于页面”里渲染还有一个放到项目文档目录用于审计时交付。前两个是市场审核实际会检查的第三个是给法务和交付团队留存的。整个过程在 CI 里一次性完成不需要人工介入。这里强烈建议不要在 UI 层面用 WebView 渲染声明文件鸿蒙适配版的 WebView 和 Android 有差异部分 API 行为不同直接用原生的 ListView 或 Markdown 渲染组件更可靠。既减少依赖又降低审核风险。5.3 下一步可扩展的两个方向完成基础适配之后还有两个可以继续深挖的方向。第一个是接入更标准化的报告格式。目前我们用的是自研的 Markdown 和 JSON后续可以考虑输出 SPDX 文档或 CycloneDX SBOM 格式这样审计结果能直接被专门的合规工具读取跨平台复用价值更高。尤其是面对政企客户时他们往往有自己的软件物料清单收集系统一份标准格式的报告能省去大量的表格转换工作。第二个是针对鸿蒙原生依赖做更深层扫描。Flutter 插件经常会带入 C/C 底层库这些库的许可证信息不在pubspec.lock里需要借助鸿蒙构建系统的产物去分析。这项工作比 Flutter 层适配更繁琐但也是政企项目中审查方最在意的一块。我们的计划是在现有命令行工具里增加一个--scan-ohos-binary参数专门扫描.so文件的元信息和许可证头目前还在验证阶段。这两个方向我们只完成了第一个的调研。如果你也在做同类工作建议先解决“能用”再追求“好用”不要一上来就铺开大改毕竟 license_checker 的核心价值在于稳定和省心而不是功能炫技。我在这次适配中还有一个小技巧值得分享调试阶段把-DOHOStrue写进 VS Code 的 launch.json这样本地调试和真机调试走同一套鸿蒙逻辑不会出现“开发机正常、真机异常”的割裂。整个改造过程用了不到两周真正耗时的地方不在代码量而是在识别那些“原本以为理所当然”的路径和权限假设。库本身不复杂复杂的是它所立足的平台生态变了。