纯Dart编写自动化打包脚本:彻底告别手动发版 简介一份使用纯Dart语言编写的自动化打包上线脚本面向需要简化软件发布流程的Dart/Flutter开发者。脚本完整覆盖命令行调用、pubspec.yaml配置、Flutter构建、环境变量管理、Git版本控制、CI/CD集成、签名处理、文件操作及云服务分发等关键环节代码结构清晰配有说明文档可直接参考或按项目定制。压缩包共34个文件以dart源码为主26个另含yaml配置、markdown说明等整体仅35KB轻量易读。已有385人学习浏览适合正在学习Dart实战及希望搭建自动化发版管线的中高级开发者。 做了挺长时间 Flutter 开发之后我有一个很强烈的感受很多团队把精力都花在 UI 和业务逻辑上但发版流程一直停留在“手动点按钮”的阶段。这个项目就是在这样的背景下做的——用纯 Dart 语言写一个自动化打包上线脚本把客户端在发版阶段的重复劳动全部吃掉。整套脚本虽然叫 dart_build_script但它不依赖 Flutter 引擎也不依赖任何 GUI 环境就是一套跑在命令行里的 Dart 程序。你给它一个版本号、一个平台参数它就能自动完成编译、打包、上传、通知这一整套动作。对每天被发版折磨的客户端开发来说这东西能直接把早上的时间从半小时压缩到两三分钟。这套脚本适合两类人看一类是天天在手动发版、被测试同事催包的客户端开发另一类是学了 Dart 语法但不知道它除了 Flutter 还能干什么的同学。我下面讲的不是概念全是在真实项目中踩过坑之后沉淀出来的实操心得。1. 为什么用纯 Dart 写自动化打包脚本1.1 背景手动发版是真疼先说说我为什么会写这个脚本。我们团队的项目迭代节奏差不多两周一个版本每次发版都要走这个链路改版本号 → 跑 flutter build → 打 dSYM → 上传到后端服务器 → 在群里 测试。其中“传包”这一步尤其烦服务器是内网的得先连内网再走 rsync 或者调试工具传上去。Android 包和 iOS 包的命令还不一样Windows 和 macOS 上的 shell 语法也有差异。刚开始我是用 Shell 脚本写的当时觉得挺顺后来遇到几个问题就开始难受了一是 Shell 在字符串处理上太弱版本号解析、JSON 拼接容易写错二是跨平台不友好Linux 上好好的脚本拿到 macOS 上路径就裂开三是团队成员想改点逻辑面对一坨 sed 和 awk 直接劝退。后来我把目光转向了 Dart毕竟团队所有人都会 Dart与其让大伙学 Shell不如直接复用现有技术栈。1.2 方案对比Shell、Python、Dart 怎么选有个朋友问我这种脚本用 Python 不香吗Python 确实可以而且生态很好。但我们的情况比较特殊客户端团队的机器上有的是 Windows有的是 macOS装 Dart SDK 是天然的因为要开发 Flutter但装 Python 还得管理版本。更关键的是Dart 本身编译成原生可执行文件之后丢给没装 SDK 的同事也能直接跑分发成本极低。我把三种方案的取舍整理成了一个表方便你按自己团队情况选对比维度Shell 脚本Python 脚本Dart 脚本学习成本低但写复杂逻辑容易翻车低中但 Flutter 团队零成本跨平台一致性差bash/zsh/cmd 差异多中依赖解释器环境好模拟器一致性强字符串与文件处理弱强强分发方式源码直接跑需要目标机器配 Python可编译成单文件可执行程序团队维护意愿低中高尤其 Flutter 团队对我个人来说选择 Dart 还有一个隐藏理由它是强类型语言写错了在编译阶段就能发现不会像 Shell 那样运行到一半才报错。对一个凌晨才想起来发版的打工人来说编译期报错比运行期爆炸友好太多了。1.3 这套脚本的核心能力整个脚本解决的核心场景有两个。第一个是本地一键构建你不需要记住 flutter build apk --release 后面那串参数也不用担心忘掉 --dart-defineAPI_ENVprod。第二个是自动上传与通知构建完成后脚本会读取配置文件里的上传地址、把产物传上去并且通过 Webhook 向即时通讯群推送一条消息内容包含版本号、构建时间、下载链接。一旦这套东西跑起来发版就不是一个技术活了而是一个“输入版本号回车”的机械动作。2. 脚本整体设计与核心模块2.1 项目结构一个单独存活的脚本工程写这个脚本没有把它塞进 Flutter 项目的 tool 目录就算完我给它的定位是一个独立工程。这么做的好处是它不参与 Flutter 的依赖解析不会被 install 的版本链影响逻辑上也跟主工程解耦后续想抽出来做成公司内部的公共工具直接拎包走人。实际目录结构如下dart_build_script/ ├── pubspec.yaml ├── bin/ │ └── build.dart # 入口文件 ├── lib/ │ ├── config.dart # 读取配置文件 │ ├── logger.dart # 带颜色的日志输出 │ ├── shell.dart # 封装进程执行 │ ├── version.dart # 版本号处理 │ └── uploader.dart # 上传与通知 ├── config/ │ └── build_config.json # 各环境配置 └── README.md为什么把模块拆成这样我的原则是一个文件只干一件事。sys 文件负责跟系统打交道config 文件负责所有参数updater 文件只关心上传和推送。这样当一个环节出问题时排查范围是被限死的。2.2 几个被低估的 Dart 内置能力Dart 的 SDK 其实藏着很多适合写 CLI 工具的内置能力不依赖任何第三方包。第一个是Process.run它可以同步或异步地执行外部命令。我封装了一层Shell.run这样不是直接调系统进程而是先打日志再执行这样在 CI 日志里能看到每一步在跑什么FutureProcessResult run(String command, ListString args, {String? workingDirectory}) async { logger.info([EXEC] $command ${args.join( )}); late final ProcessResult result; if (workingDirectory null) { result await Process.run(command, args); } else { result await Process.run(command, args, workingDirectory: workingDirectory); } if (result.exitCode ! 0) { logger.error([FAIL] ${result.stderr}); throw ProcessException(command, args, result.stderr as String?, result.exitCode); } logger.info([OK] ${result.stdout}); return result; }第二个是专门用于读取文件内容的File和Directory它们完全是跨平台的。你不需要自己拼路径尤其不要用字符串硬编码/或者\Dart 的File在 Windows 和 macOS 上都能正确处理相对路径。第三个是dart:io包里的HttpClient。上传包体我用它发 multipart/form-data 请求推送通知我用它发 POST JSON完全不需要引入http或者dio因为脚本里用不到复杂的拦截器内置的这个客户端够用了。2.3 版本管理让版本号可控可回退发版最重要的是版本号不能乱。我设计了两个规则版本号统一从 pubspec.yaml 读取。构建时把version: 1.2.345解析出来1.2.3作为展示版本45作为构建号iOS 的 CFBundleVersion 和 Android 的 versionCode 都跟它对齐。每次发版前自动 git tag。脚本在构建成功后打一个v1.2.345的 tag推送到远程仓库。以后任何一个包出了线上问题都能通过 tag 找到当时对应的代码提交。版本号解析的核心代码大概长这样String parseVersionFromPubspec(String content) { final match RegExp(r^version:\s*([^\n])$, multiLine: true).firstMatch(content); if (match null) { throw StateError(pubspec.yaml 中找不到 version 字段); } return match.group(1)!.trim(); }这个正则看着简单但有个细节值得注意我用了multiLine: true这样^和$会匹配每一行的开头和结尾而不是整个字符串的开头和结尾。不熟悉 Dart 正则的同学经常在这一步翻车匹配出来的结果是一大段内容。3. 实操过程从初始化到真正跑通一套流程3.1 项目初始化别忘记 Dart 的入口约定初始化一个 Dart CLI 项目其实就两条命令dart create -t console dart_build_script cd dart_build_script注意-t console这个模板是专门用于命令行程序的它会自动生成bin/目录和一个包含main函数的文件。这里有个 Dart 的硬性规定一个可以被直接运行的程序必须有一个入口函数叫main否则运行时会直接报invoked dart programs must have a main function defined。很多新手反手把入口文件改名为build.dart但忘了函数名要保持main结果就卡在这里。如果你不想用模板直接手动创建bin/build.dart也是可以的只要确保文件内容里有一个顶层main函数就行Futurevoid main(ListString arguments) async { // 整个脚本从这里开始 }3.2 实现核心构建流程构建流程我拆成了四个步骤解析参数 → 加载配置 → 执行构建 → 处理产物。参数解析我用的是dart:io的Platform.executableArguments简单直接不需要引args包因为我的参数就两三个不会膨胀。完整参数定义大概是这样的参数名含义示例--platform目标平台android或ios--mode构建模式release或debug--env环境配置名prod、dev、test--skip-upload跳过上传本地调试时用构建核心代码这里贴一段能说明问题就行final args parseArguments(arguments); final config loadConfig(config/build_config_${args.env}.json); var command flutter; var commandArgs String[ build, args.platform, --${args.mode}, --dart-defineAPI_ENV${args.env}, ]; if (args.platform android) { commandArgs.add(--split-per-abi); // 拆出 armeabi-v7a、arm64-v8a } await shell.run(command, commandArgs, workingDirectory: projectRoot);为什么 Android 打包要加--split-per-abi原因很简单不信你看应用商店的包体大小限制。一个 fat APK 把所有 CPU 架构全打进去动辄上百 MB加上--split-per-abi之后一个架构一个包测试下载和上传都快很多。当然这也会带来一个坑后面我放在第 4 节说。3.3 上传与通知把脚本接入团队协作构建完成之后的产物在 Android 上是build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk之类的路径在 iOS 上是build/ios/iphoneos/Runner.ipa。脚本要把这些产物按约定重命名比如MyApp_v1.2.3_arm64.apk然后上传到配置好的服务器。这里我直接使用一个简单的 HTTP PUT 请求来传输文件避免引入 rsync 这种外部依赖。上传的代码Futurevoid uploadFile(File file, String uploadUrl) async { final client HttpClient(); final request await client.putUrl(Uri.parse(uploadUrl)); request.headers.set(HttpHeaders.contentTypeHeader, application/octet-stream); request.contentLength file.lengthSync(); await request.addStream(file.openRead()); final response await request.close(); print(upload status: ${response.statusCode}); client.close(); }通知部分更简单就是拼一段 JSON往群机器人的 Webhook 发一个 POST。这里有一个特别容易踩的坑HTTP 请求的Content-Type必须是application/json而且 JSON 字符串里的换行符要处理好。Dart 的jsonEncode会把特殊字符自动转义所以放心用不要自己手拼字符串。3.4 一次完整的运行过程配置和代码都准备好之后实际的使用体验就是一行命令dart run bin/build.dart --platform android --mode release --env prod脚本输出大概长这样[INFO] Loading config: config/build_config_prod.json [INFO] Current version: 1.2.345 [EXEC] flutter build apk --release --dart-defineAPI_ENVprod --split-per-abi [INFO] Build succeeded in 3m12s [INFO] Renaming artifact to: MyApp_v1.2.3_arm64.apk [UPLOAD] Uploading to https://download.example.com/MyApp_v1.2.3_arm64.apk [INFO] Upload finished, 56.8 MB [NOTIFY] Webhook sent, status 200 [INFO] Tagging commit: v1.2.345到这一步一条完整的发版链路就跑通了。之后测试同事拿到的下载链接就是脚本自动推送的那条消息他们甚至感知不到“我正在发版”这个过程。4. 常见问题与排查技巧实录4.1 invoked dart programs must have a main function defined这个报错是 Dart 新手最常见的一个我在项目分享里被问了至少五次。它出现的原因有三个bin/build.dart文件里没有声明main函数。声明了但拼写错误比如mian、Main。用dart run bin/build.dart运行了但bin/build.dart的内容实际上是从其他文件 import 进来的代码而没有定义main。检查顺序也按上面来先看文件里有没有void main()再确认拼写最后确认入口函数是在被运行的文件里而不是在 import 的库文件里。4.2 Windows 与 macOS 路径分隔符的坑Dart 的File和Directory会自动处理路径分隔符但要注意你如果直接拼接字符串比如build/app/outputs/...这种带正斜杠的路径在 Windows 上是能跑的因为 Dart 的File会自动把/转成\。但如果是反过来你在 macOS 上写死了\\那就会出问题。我自己的经验是统一写正斜杠/不要用反斜杠。如果需要拼接路径用package:path的join方法它会在不同平台上生成正确的分隔符。不要自己去判断Platform.isWindows然后拼不同的字符串代码会变得很难看。4.3 环境变量与密钥别把密码写在代码里上传服务器需要 username/password或者 token。最忌讳的是把这些信息直接写在build_config.json里因为配置文件一般是提交到 git 仓库的。我的方案是配置文件里只写“从哪个环境变量取密钥”的 key 名字脚本运行时再从Platform.environment读取。final token Platform.environment[UPLOAD_TOKEN]; if (token null || token.isEmpty) { throw StateError(环境变量 UPLOAD_TOKEN 未设置); }这样配置仓库是干净的密钥只存在于本机或 CI 平台的 secrets 里。如果你不小心把密钥提交了一定要去平台撤销掉不要以为删掉文件就没事了git 历史里还留着。4.4 Android 拆分架构后产物文件变多前面我提到--split-per-abi会把 APK 拆成多个实际产物是这样的app-armeabi-v7a-release.apk app-arm64-v8a-release.apk app-x86_64-release.apk如果你把它们都传上去了那测试可能会下载错文件。我的处理方式是脚本里判断一下产物数量如果大于 1就只选择arm64-v8a的那个作为默认包因为现在市面上绝大多数真机都是 arm64 架构。x86_64的包留给模拟器用户但不会主动推消息。5. 实测心得与后续可以继续做的方向脚本目前在团队里跑了三个多月最大的感受就是“省事”。以前测试同事总在群里问包呢包呢现在到点自动推送消息没人催了。我自己也能把早上到公司的半小时拿来干点正事而不是对着命令行手忙脚乱。另外我建议刚接触这类脚本的同学不要一上来就想把什么都自动化。先用手动命令把流程走通再把跑过次数最多的那一段抽出来写脚本最后再考虑加触发器、加 CI。步子迈太大出了问题你连手动操作的路径都忘了那就更难受了。这个脚本我后续还打算扩展两个方向一个是接入 CI提交 tag 时自动触发构建构建完自动上传另一个是多项目支持现在配置是写死在文件里的后续打算让命令支持同时传产品名这样公司里其他项目组也能直接用同一套工具。目前这两个方向都在开发中等跑通了再来分享细节。最后再讲一个很实用的点脚本里所有耗时操作都一定要把进度“打印”出来不是让你用print随便打而是将每步耗时也输出。实测下来这种日志风格对排查问题帮助极大。有一次构建一直卡住我一眼就看到是flutter build那一步卡了 20 分钟没有日志输出最后定位到是 CI 机器的磁盘满了当时如果没打时间戳排查少说要再花半天。本文还有配套的精品资源点击获取

相关新闻

最新新闻

AI产品经理如何做Agent评测:从维度设计到闭环落地

AI产品经理如何做Agent评测:从维度设计到闭环落地

评测这事卡住了很多 AI 产品经理:Demo 阶段跑通一次很容易,真到上线前验收,同一个 Agent 换个说法就不会了,或者同一批用例昨天能过今天过不了,再或者 Agent 明明调用了正确工具,结果却答非所问。这里面的难…

2026/9/9 18:17:10
Markdown 全解析:语法原理、编辑器选型与渲染转换

Markdown 全解析:语法原理、编辑器选型与渲染转换

Markdown 这玩意儿,说实话我用了快十年了,从一开始以为它只是个“简化版 HTML”,到后来把它当成笔记、博客、文档、甚至工作流里不可或缺的一环,中间踩过不少坑,也慢慢摸清楚了它的脾气。很多人把它想得太神秘&#xf…

2026/9/9 18:17:10
用AI写五子棋App实战:从0到1的Android开发避坑指南

用AI写五子棋App实战:从0到1的Android开发避坑指南

用 AI 写一个五子棋 App,现在已经不是"能不能做"的问题了,而是"怎么少踩坑"的问题。我最近用 open claw(其实就是 Claude,中文圈习惯这么叫它)从零写了一个简易版五子棋 Android 应用,…

2026/9/9 18:17:10
数据资产内容创作实战:从信息筛选到摘要联播的完整方法论

数据资产内容创作实战:从信息筛选到摘要联播的完整方法论

做这期内容的时候,我刚从三个小时的素材整理中爬出来。2026年3月11日,第16期,这个编号看起来已经是个有点分量的数字了,但实际上只有我自己清楚,从第一期的手忙脚乱到现在的相对从容,这中间踩了多少坑、推倒…

2026/9/9 18:17:10
AI把论文写得又快又顺,才是最危险的信号:毕业论文全流程工具选型与搭配清单

AI把论文写得又快又顺,才是最危险的信号:毕业论文全流程工具选型与搭配清单

又到开题、初稿、降重连轴转的季节。很多同学用AI写论文时都会遇到几个经典问题:选题越聊越空、参考文献真假难辨、图表公式还要自己另做、格式改到崩溃,最后查重和AIGC检测还像开盲盒。 其实,没有任何一个工具能包打天下。真正高效的用法&a…

2026/9/9 18:17:10
用 QtScrcpy 在电脑上显示与控制安卓:连接、键鼠映射与组控指南

用 QtScrcpy 在电脑上显示与控制安卓:连接、键鼠映射与组控指南

用 QtScrcpy 在电脑上显示与控制安卓:连接、键鼠映射与组控指南 【免费下载链接】QtScrcpy Android real-time display control software 项目地址: https://gitcode.com/GitHub_Trending/qt/QtScrcpy QtScrcpy 是一款基于 Qt 框架的开源安卓投屏与控制工具&…

2026/9/9 18:12:10