dart-build-cli-app

dart-build-cli-app

热门

入口点结构、退出码、跨平台脚本。用于构建命令行工具、脚本或应用程序。

392Star
25Fork
更新于 2026/7/10
SKILL.md
readonly只读
name
dart-build-cli-app
description

入口点结构、退出码、跨平台脚本。用于构建命令行工具、脚本或应用程序。

构建 Dart CLI 应用程序

目录

项目设置与架构

使用官方 Dart 模板初始化新的 CLI 项目,以确保标准目录结构。

  • 运行 dart create -t cli <project_name> 来搭建一个包含基本参数解析的控制台应用程序。
  • 将可执行入口点(包含 main() 的文件)专门放在 bin/ 目录下。
  • 将内部实现逻辑放在 lib/src/ 中,并通过 lib/<project_name>.dart 暴露公共 API。
  • 在 CI 环境中通过运行 dart format . --set-exit-if-changed 强制执行代码格式化。如果存在格式违规,该命令返回退出码 1。

参数解析与命令路由

导入 args 包来管理命令行参数、标志和子命令。

  • 如果构建简单脚本:直接使用 ArgParser 定义标志(addFlag)和选项(addOption)。
  • 如果构建复杂的多命令 CLI(如 git):实现 CommandRunner 并为每个子命令扩展 Command
  • CommandRunner.argParser 上定义全局参数,在单个 Command.argParser 上定义命令特定参数。
  • 捕获 UsageException 以优雅地处理无效参数并显示自动生成的帮助文本。
  • 验证帮助文本的准确性:确保帮助文本提供运行工具所需的所有信息。如果帮助文本引用了编译后的可执行文件名,并且用户需要将其添加到 PATH 才能以这种方式运行,请在帮助文本或描述中提供清晰的说明。

执行与错误处理

利用 iostack_trace 包构建健壮、生产就绪的 CLI 工具。

  • 使用 io 包的 ExitCode 枚举返回标准的 POSIX 退出码(例如 ExitCode.success.codeExitCode.usage.code)。
  • 如果多个异步监听器需要顺序访问标准输入,请使用 io 包的 sharedStdIn
  • 使用 stack_trace 包的 Chain.capture() 包装应用程序执行,以跟踪异步堆栈链。
  • 使用 Trace.terseChain.terse 格式化输出堆栈跟踪,以去除嘈杂的核心库帧,并向用户呈现可读的错误信息。
  • 不要在底层逻辑或存储类中吞掉异常,除非可以恢复。让它们向上冒泡或重新抛出,以便更高级别的命令知道操作失败。
  • 快速失败并返回非零退出码:确保操作失败时向 stderr 输出描述性错误消息,并返回适当的非零退出码(例如,使用 exit(1) 或在捕获 UsageException 后触发退出码 64)。

测试 CLI 应用程序

[!IMPORTANT]
所有新命令和重要功能必须由自动化测试覆盖。 手动验证不足以测试逻辑。但是,仍然需要手动验证帮助文本和用户体验(UX),以确保界面直观且正确。

使用 test_processtest_descriptor 为 CLI 编写高保真集成测试。

  • 使用 test_descriptord.dird.file)定义预期的文件系统状态。
  • 在执行前使用 await d.Descriptor.create() 创建模拟文件系统。
  • 使用 TestProcess.start('dart', ['run', 'bin/cli.dart', ...args]) 启动 CLI 进程。
  • 使用 StreamQueue 匹配器(例如 emitsThroughemits)验证标准输出和错误流。
  • 使用 await process.shouldExit(0) 断言最终退出码。
  • 使用 await d.Descriptor.validate() 验证结果文件系统的变更。

编译与分发

根据分发需求选择合适的编译目标。

  • 如果在开发期间本地测试: 使用 dart run bin/cli.dart。这使用 JIT 编译器进行快速迭代。
  • 如果需要打包代码资产和动态库: 使用 dart build cli。这会运行构建钩子并输出到 build/cli/_/bundle/
  • 如果需要分发独立的原生可执行文件: 使用 dart compile exe bin/cli.dart -o <output_path>。这会将 Dart 运行时和机器代码打包到单个文件中。
  • 如果需要分发多个应用程序且磁盘空间严格受限: 使用 dart compile aot-snapshot bin/cli.dart。使用 dartaotruntime 运行生成的 .aot 文件。

<details>
<summary>交叉编译目标(仅限 Linux)</summary>

Dart 支持从 macOS、Windows 或 Linux 主机交叉编译到 Linux。
dart compile exedart compile aot-snapshot 中使用 --target-os--target-arch 标志。

  • --target-os=linux(目前仅支持 Linux 作为交叉编译目标)
  • --target-arch=arm64(64 位 ARM)
  • --target-arch=x64(x86-64)
  • --target-arch=arm(32 位 ARM)
  • --target-arch=riscv64(64 位 RISC-V)

示例:dart compile exe --target-os=linux --target-arch=arm64 bin/cli.dart
</details>

工作流

任务进度:实现一个新的 CLI 命令

  • [ ] 在 lib/src/commands/ 中创建一个继承 Command 的新类。
  • [ ] 定义 namedescription 属性。
  • [ ] 在构造函数中使用 argParser.addFlag()argParser.addOption() 注册命令特定的标志。
  • [ ] 实现包含核心逻辑的 run() 方法。
  • [ ] 在 bin/cli.dart 中使用 addCommand() 将新命令注册到 CommandRunner 实例中。
  • [ ] 在 test/ 目录中使用 test_process 或标准测试为新命令创建测试。
  • [ ] 运行验证器 -> 执行 dart run bin/cli.dart help <command_name> 以验证帮助文本生成。
  • [ ] 验证最终用户体验:使用 dart compile exe 编译应用程序并运行生成的可执行文件,以验证目标用户体验(例如 ./bin/cli <command>)。

任务进度:编译并发布原生可执行文件

  • [ ] 运行验证器 -> 执行 dart format . --set-exit-if-changed 以确保代码格式化。
  • [ ] 运行验证器 -> 执行 dart analyze 以确保没有静态分析错误。
  • [ ] 运行验证器 -> 执行 dart test 以通过所有集成测试。
  • [ ] 为主机操作系统编译:dart compile exe bin/cli.dart -o build/cli-host
  • [ ] 为 Linux 编译(如果主机是 macOS/Windows):dart compile exe --target-os=linux --target-arch=x64 bin/cli.dart -o build/cli-linux-x64

示例

示例:CommandRunner 实现

import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:stack_trace/stack_trace.dart';

class CommitCommand extends Command {
  @override
  final String name = 'commit';
  @override
  final String description = '记录对仓库的更改。';

  CommitCommand() {
    argParser.addFlag('all', abbr: 'a', help: '提交所有更改的文件。');
  }

  @override
  Future<void> run() async {
    final commitAll = argResults?['all'] as bool? ?? false;
    print('正在提交... (全部: $commitAll)');
  }
}

void main(List<String> args) {
  Chain.capture(() async {
    final runner = CommandRunner('dgit', '分布式版本控制。')
      ..addCommand(CommitCommand());

    await runner.run(args);
  }, onError: (error, chain) {
    if (error is UsageException) {
      stderr.writeln(error.message);
      stderr.writeln(error.usage);
      exit(64); // ExitCode.usage.code
    } else {
      stderr.writeln('致命错误: $error');
      stderr.writeln(chain.terse);
      exit(1);
    }
  });
}

示例:使用子进程进行集成测试

import 'package:test/test.dart';
import 'package:test_process/test_process.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

void main() {
  test('CLI 正确格式化输出并修改文件系统', () async {
    // 1. 设置模拟文件系统
    await d.dir('project', [
      d.file('config.json', '{"key": "value"}')
    ]).create();

    // 2. 启动 CLI 进程
    final process = await TestProcess.start(
      'dart',
      ['run', 'bin/cli.dart', 'process', '--path', '${d.sandbox}/project']
    );

    // 3. 验证 stdout 流
    await expectLater(process.stdout, emitsThrough('处理完成。'));

    // 4. 验证退出码
    await process.shouldExit(0);

    // 5. 验证文件系统变更
    await d.dir('project', [
      d.file('config.json', '{"key": "value"}'),
      d.file('output.log', '成功')
    ]).validate();
  });
}