HarmonyOS 7 API26 build-profile 升级避坑:targetSdkVersion、product 与 CI 一起收住

适用范围:HarmonyOS 7 / API 26,Stage 模型工程,重点关注 build-profile.json5、module.json5、DevEco Studio / Hvigor 构建链路。
核心结论:升级不是只改一个数字,而是要把版本字段、product 配置、模块配置和 CI 检查放到同一套规则里。

HarmonyOS 7 API 26 build-profile 升级检查示意图

先说问题:升级到 API 26 后,构建配置别再靠感觉改

项目升级到 HarmonyOS 7 / API 26 时,很多人第一反应是先改 SDK,再跑一遍构建。真正容易卡住的地方不一定在业务代码,而是在 build-profile.json5 这类工程配置里。

API 26 以后,工程配置里一部分版本字段更适合按完整字符串写清楚,比如 targetSdkVersion 用 “26.0.0” 表达,比只写 26 更适合团队协作和 CI 检查。这个变化看着很小,但放到多人项目里,很容易出现本地能跑、CI 失败,或者不同 module 版本写法不一致。

这篇只拆一个问题:API 26 工程配置升级时,怎么把版本号、product、module 和 CI 检查收住。

环境和版本

项目 取值
系统方向 HarmonyOS 7 / API 26
工程工具 DevEco Studio / Hvigor
配置文件 build-profile.json5、module.json5
目标 升级前能检查,升级后能复现,CI 里能拦住错误配置

问题是怎么发生的

很多项目早期配置是这样写的:

{
  "app": {
    "products": [
      {
        "name": "default",
        "compileSdkVersion": 12,
        "compatibleSdkVersion": 12,
        "targetSdkVersion": 12
      }
    ]
  }
}

升级到 API 26 后,如果只把数字改大,表面上像是完成了:

{
  "app": {
    "products": [
      {
        "name": "default",
        "compileSdkVersion": 26,
        "compatibleSdkVersion": 12,
        "targetSdkVersion": 26
      }
    ]
  }
}

问题在于,这种改法没有表达出 26.0.0 的完整版本语义,也容易跟其它 module 的写法不一致。项目越大,越不能靠人工扫配置。

案例一:旧写法为什么要被拦住

下面这个脚本不是替代 Hvigor,而是放在提交前或 CI 前面做第一道检查。它只做几件事:targetSdkVersion 必须是字符串,compileSdkVersion 必须和目标版本一致,compatibleSdkVersion 不能高于 targetSdkVersion。

interface ProductConfig {
  name: string
  compileSdkVersion: string | number
  compatibleSdkVersion: string | number
  targetSdkVersion: string | number
}

function versionToNumber(value: string | number): number {
  const text = String(value)
  return Number(text.split(".")[0])
}

function checkApi26Product(product: ProductConfig) {
  const problems: string[] = []
  if (!product.name || product.name.trim().length === 0) {
    problems.push("product.name 不能为空")
  }
  if (typeof product.targetSdkVersion !== "string") {
    problems.push("API 26 起 targetSdkVersion 建议写成字符串,例如 26.0.0")
  }
  if (String(product.compileSdkVersion) !== "26.0.0") {
    problems.push("compileSdkVersion 没有锁到 26.0.0")
  }
  const compatible = versionToNumber(product.compatibleSdkVersion)
  const target = versionToNumber(product.targetSdkVersion)
  if (compatible > target) {
    problems.push("compatibleSdkVersion 不能高于 targetSdkVersion")
  }
  return problems
}

把旧配置丢进去,结果会很直接:

const legacyProduct = {
  name: "default",
  compileSdkVersion: 26,
  compatibleSdkVersion: 12,
  targetSdkVersion: 26
}
console.log(checkApi26Product(legacyProduct))

输出:

[
  "API 26 起 targetSdkVersion 建议写成字符串,例如 26.0.0",
  "compileSdkVersion 没有锁到 26.0.0"
]

这个结果说明问题不是“能不能写数字”,而是团队要不要把升级规则写死。只靠口头约定,迟早会有人在某个分支里写回旧格式。

案例二:推荐写法怎么进入 CI

我更建议把 API 26 配置写成明确版本:

{
  "app": {
    "products": [
      {
        "name": "default",
        "compileSdkVersion": "26.0.0",
        "compatibleSdkVersion": "12.0.0",
        "targetSdkVersion": "26.0.0"
      }
    ]
  }
}

再把检查脚本接到 CI。比如用一个最小 Node 脚本读取 build-profile.json5,解析后逐个 product 检查:

import { readFileSync } from "node:fs"
const raw = readFileSync("build-profile.json5", "utf-8")
const normalized = raw.replace(///.*$/gm, "").replace(/,\s*([}\]])/g, "$1")
const profile = JSON.parse(normalized)
const products = profile.app?.products ?? []
let failed = false
for (const product of products) {
  const problems = checkApi26Product(product)
  if (problems.length > 0) {
    failed = true
    console.error("[build-profile] " + (product.name || "unknown") + ":")
    for (const problem of problems) console.error("- " + problem)
  }
}
if (failed) process.exit(1)
console.log("[build-profile] API 26 配置检查通过")

推荐写法的输出应该是:

[build-profile] API 26 配置检查通过

三种处理方式怎么选

方案 优点 问题 适合场景
只手动改配置 容易漏 module、漏 product 一次性小 Demo
在 README 写规范 成本低 没有强约束 小团队过渡期
CI 加检查脚本 能挡回归 初次要写脚本 正式项目、多人协作、活动文章 Demo

我的选择是第三种。API 26 的升级点很多,越是工具链和配置类问题,越不应该靠人肉检查。

可以封装成什么

可以把检查逻辑单独放到 scripts/check-api26-profile.ts:

export function assertApi26BuildProfile(profile: any) {
  const products = profile.app?.products ?? []
  const errors: string[] = []
  for (const product of products) {
    const problems = checkApi26Product(product)
    errors.push(...problems.map((item) => product.name + ": " + item))
  }
  if (errors.length > 0) {
    throw new Error(errors.join("\n"))
  }
}

然后在本地 precheck、CI、发版前检查里复用。这样后面再升级 API 27,也只是扩展规则,不需要每个项目都重新写一遍。

最后怎么验收

我会用四个结果判断这次升级有没有收住:旧数字写法能被脚本拦住;targetSdkVersion 的 26.0.0 能稳定通过;多 product 工程里,每个 product 都被检查;CI 输出能直接告诉你是哪一个字段错了,而不是只给一个构建失败。

这类文章解决的是升级工程里最常见的一类问题:规则没人记得住,脚本能记住。API 26 升级前,先把配置入口守住,后面排查编译、权限、审核问题才不会混在一起。

再补一层:为什么 product 会把问题放大

单 product 工程里,targetSdkVersion 写错通常很快能发现;多 product 工程就不一样了。比如一个项目同时有 phone、tablet、wearable 三套 product,本地开发只跑 phone,CI 或发版时才跑全量构建,错误就会晚很多才暴露。

可以用下面这个例子复现:

{
  "app": {
    "products": [
      {
        "name": "phone",
        "compileSdkVersion": "26.0.0",
        "targetSdkVersion": "26.0.0"
      },
      {
        "name": "tablet",
        "compileSdkVersion": "26.0.0",
        "targetSdkVersion": 26
      },
      {
        "name": "wearable",
        "compileSdkVersion": "26.0.0",
        "targetSdkVersion": "25.0.0"
      }
    ]
  }
}

这里有两个坑:tablet 用了数字写法,wearable 还停在旧目标版本。只看 phone 是发现不了的,所以检查脚本必须遍历每一个 product。

把失败报告写清楚

CI 里最怕只看到一句“构建失败”。我会让检查脚本输出到字段级别:

type ProductConfig = {
  name: string
  compileSdkVersion: string | number
  targetSdkVersion: string | number
}

function expectVersionString(value: string | number, field: string, productName: string): string[] {
  const errors: string[] = []
  if (typeof value !== 'string') {
    errors.push(productName + '.' + field + ' 必须写成字符串,例如 26.0.0')
    return errors
  }
  if (!/^26\.0\.0$/.test(value)) {
    errors.push(productName + '.' + field + ' 当前是 ' + value + ',需要对齐到 26.0.0')
  }
  return errors
}

function checkProducts(products: ProductConfig[]): string[] {
  const errors: string[] = []
  products.forEach((product) => {
    errors.push(...expectVersionString(product.compileSdkVersion, 'compileSdkVersion', product.name))
    errors.push(...expectVersionString(product.targetSdkVersion, 'targetSdkVersion', product.name))
  })
  return errors
}

这段代码不是为了替代 Hvigor 构建,而是让错误提前出现。构建失败以后再查配置,成本比提交前拦截高得多。

两个验收用例

用例一:版本字段写法错误

输入:

const products = [
  { name: 'phone', compileSdkVersion: '26.0.0', targetSdkVersion: 26 }
]
console.log(checkProducts(products))

预期输出:

phone.targetSdkVersion 必须写成字符串,例如 26.0.0

这个用例解决的是“本地写 26 也能看懂,为什么 CI 要拦”的争议。团队规则要稳定,就不要让同一个字段出现两种写法。

用例二:多 product 没有一起升级

输入:

const products = [
  { name: 'phone', compileSdkVersion: '26.0.0', targetSdkVersion: '26.0.0' },
  { name: 'wearable', compileSdkVersion: '26.0.0', targetSdkVersion: '25.0.0' }
]
console.log(checkProducts(products))

预期输出:

wearable.targetSdkVersion 当前是 25.0.0,需要对齐到 26.0.0

这个用例解决的是“只测默认 product 导致其它设备形态漏升级”的问题。多设备应用尤其要注意这一点,折叠屏、平板、穿戴或鸿蒙电脑 product 不能只靠人工记忆。

我会把这条规则放在哪里

阶段 检查内容 失败后怎么处理
本地提交前 targetSdkVersion、compileSdkVersion 写法 直接阻断提交
CI 构建前 所有 product 是否都对齐 API 26 阻断流水线
发版前 module、product、权限声明是否一致 进入发版检查清单

这样做的好处是,配置问题不会拖到上架审核前才暴露。API 26 升级不是只改一个数字,而是把工程里所有会影响构建、适配和审核的版本入口一起收住。

后面怎么避免反复出错

我会把版本配置检查做成一个单独脚本,放到工程的 tools 目录里,CI 和本地都调用同一份逻辑。以后升级 API 27、API 28,只需要改允许版本和规则提示,不需要每个模块都重新排查。

这类检查越早自动化,越不容易出现“我本地没问题,流水线怎么挂了”的情况。尤其是多 product、多设备形态的 HarmonyOS 工程,配置一致性本身就是稳定性的一部分。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐