dart-build-cli-app

Par flutter · skills

Structure de point d'entrée, codes de sortie, scripts multiplateformes. À utiliser lors de la création d'utilitaires en ligne de commande, de scripts ou d'applications.

npx skills add https://github.com/flutter/skills --skill dart-build-cli-app

Construire des applications CLI en Dart

Contenu

Configuration du projet et architecture

Initialisez les nouveaux projets CLI en utilisant le template officiel Dart pour garantir des structures de répertoires standard.

  • Exécutez dart create -t cli <project_name> pour générer une application console avec analyse basique des arguments.
  • Placez les points d'entrée exécutables (fichiers contenant main()) exclusivement dans le répertoire bin/.
  • Placez la logique d'implémentation interne dans lib/src/ et exposez les APIs publiques via lib/<project_name>.dart.
  • Imposez le formatage dans les environnements CI en exécutant dart format . --set-exit-if-changed. Cela retourne le code de sortie 1 s'il existe des violations de formatage.

Analyse des arguments et routage des commandes

Importez le package args pour gérer les arguments de ligne de commande, les flags et les sous-commandes.

  • Si vous construisez un script simple : Utilisez ArgParser directement pour définir les flags (addFlag) et les options (addOption).
  • Si vous construisez une CLI multi-commandes complexe (comme git) : Implémentez CommandRunner et étendez Command pour chaque sous-commande.
  • Définissez les arguments globaux sur CommandRunner.argParser et les arguments spécifiques à une commande sur le Command.argParser individuel.
  • Capturez UsageException pour gérer gracieusement les arguments invalides et afficher le texte d'aide généré automatiquement.
  • Validez l'exactitude du texte d'aide : Assurez-vous que le texte d'aide fournit toutes les informations nécessaires pour exécuter l'outil. Si le texte d'aide référence un nom d'exécutable compilé et que l'utilisateur doit l'ajouter à son PATH pour l'exécuter de cette façon, fournissez des instructions claires sur comment le faire dans le texte d'aide ou la description.

Exécution et gestion des erreurs

Exploitez les packages io et stack_trace pour construire des outils CLI robustes et prêts pour la production.

  • Utilisez l'enum ExitCode du package io pour retourner les codes de sortie POSIX standard (par exemple, ExitCode.success.code, ExitCode.usage.code).
  • Utilisez sharedStdIn du package io si plusieurs écouteurs asynchrones ont besoin d'un accès séquentiel à l'entrée standard.
  • Encapsulez l'exécution de l'application dans Chain.capture() du package stack_trace pour tracer les chaînes de pile asynchrones.
  • Formatez les traces de pile de sortie en utilisant Trace.terse ou Chain.terse pour supprimer les frames de la bibliothèque centrale bruyants et présenter des erreurs lisibles à l'utilisateur.
  • Ne supprimez pas les exceptions dans la logique de bas niveau ou les classes de stockage à moins qu'une récupération soit possible. Laissez-les remonter ou relancez-les pour que les commandes de haut niveau sachent que les opérations ont échoué.
  • Échouez rapidement avec des codes de sortie non nuls : Assurez-vous que les défaillances d'opération entraînent des messages d'erreur descriptifs vers stderr et les codes de sortie non nuls appropriés (par exemple, en utilisant exit(1) ou en déclenchant un code de sortie 64 après une UsageException capturée).

Tests des applications CLI

[!IMPORTANT] Toutes les nouvelles commandes et fonctionnalités significatives doivent être couvertes par des tests automatisés. La vérification manuelle ne suffit pas pour tester la logique. Cependant, la vérification manuelle du texte d'aide et de l'expérience utilisateur (UX) est toujours requise pour garantir que l'interface est intuitive et correcte.

Utilisez test_process et test_descriptor pour écrire des tests d'intégration haute-fidélité pour votre CLI.

  • Définissez les états du système de fichiers attendus en utilisant test_descriptor (d.dir, d.file).
  • Créez le système de fichiers simulé avant l'exécution en utilisant await d.Descriptor.create().
  • Lancez le processus CLI en utilisant TestProcess.start('dart', ['run', 'bin/cli.dart', ...args]).
  • Validez les flux de sortie standard et d'erreur en utilisant les matchers StreamQueue (par exemple, emitsThrough, emits).
  • Affirmez le code de sortie final en utilisant await process.shouldExit(0).
  • Validez les mutations du système de fichiers résultantes en utilisant await d.Descriptor.validate().

Compilation et distribution

Sélectionnez la cible de compilation appropriée en fonction de vos exigences de distribution.

  • Si vous testez localement pendant le développement : Utilisez dart run bin/cli.dart. Cela utilise le compilateur JIT pour une itération rapide.
  • Si vous regroupez des actifs de code et des bibliothèques dynamiques : Utilisez dart build cli. Cela exécute les hooks de build et produit une sortie dans build/cli/_/bundle/.
  • Si vous distribuez un exécutable natif autonome : Utilisez dart compile exe bin/cli.dart -o <output_path>. Cela regroupe le runtime Dart et le code machine dans un seul fichier.
  • Si vous distribuez plusieurs applications avec des limites strictes d'espace disque : Utilisez dart compile aot-snapshot bin/cli.dart. Exécutez le fichier .aot résultant en utilisant dartaotruntime.

<details> <summary>Cibles de compilation croisée (Linux uniquement)</summary>

Dart supporte la compilation croisée vers Linux à partir d'hôtes macOS, Windows ou Linux. Utilisez les flags --target-os et --target-arch avec dart compile exe ou dart compile aot-snapshot.

  • --target-os=linux (Seul Linux est actuellement supporté comme cible de compilation croisée)
  • --target-arch=arm64 (ARM 64 bits)
  • --target-arch=x64 (x86-64)
  • --target-arch=arm (ARM 32 bits)
  • --target-arch=riscv64 (RISC-V 64 bits)

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

Workflows

Progression de la tâche : Implémenter une nouvelle commande CLI

  • [ ] Créez une nouvelle classe étendant Command dans lib/src/commands/.
  • [ ] Définissez les propriétés name et description.
  • [ ] Enregistrez les flags spécifiques à la commande dans le constructeur en utilisant argParser.addFlag() ou argParser.addOption().
  • [ ] Implémentez la méthode run() avec la logique principale.
  • [ ] Enregistrez la nouvelle commande dans l'instance CommandRunner dans bin/cli.dart en utilisant addCommand().
  • [ ] Créez les tests pour la nouvelle commande dans le répertoire test/ en utilisant test_process ou les tests standard.
  • [ ] Exécutez le validateur -> Exécutez dart run bin/cli.dart help <command_name> pour vérifier la génération du texte d'aide.
  • [ ] Vérifiez l'UX final : Compilez l'application en utilisant dart compile exe et exécutez l'exécutable résultant pour vérifier l'expérience utilisateur cible (par exemple, ./bin/cli <command>).

Progression de la tâche : Compiler et publier un exécutable natif

  • [ ] Exécutez le validateur -> Exécutez dart format . --set-exit-if-changed pour assurer le formatage du code.
  • [ ] Exécutez le validateur -> Exécutez dart analyze pour assurer l'absence d'erreurs d'analyse statique.
  • [ ] Exécutez le validateur -> Exécutez dart test pour réussir tous les tests d'intégration.
  • [ ] Compilez pour l'OS hôte : dart compile exe bin/cli.dart -o build/cli-host
  • [ ] Compilez pour Linux (si l'hôte est macOS/Windows) : dart compile exe --target-os=linux --target-arch=x64 bin/cli.dart -o build/cli-linux-x64

Exemples

Exemple : Implémentation de 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 = 'Record changes to the repository.';

  CommitCommand() {
    argParser.addFlag('all', abbr: 'a', help: 'Commit all changed files.');
  }

  @override
  Future<void> run() async {
    final commitAll = argResults?['all'] as bool? ?? false;
    print('Committing... (All: $commitAll)');
  }
}

void main(List<String> args) {
  Chain.capture(() async {
    final runner = CommandRunner('dgit', 'Distributed version control.')
      ..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('Fatal error: $error');
      stderr.writeln(chain.terse);
      exit(1);
    }
  });
}

Exemple : Tests d'intégration avec des sous-processus

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 formats output correctly and modifies filesystem', () async {
    // 1. Setup mock filesystem
    await d.dir('project', [
      d.file('config.json', '{"key": "value"}')
    ]).create();

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

    // 3. Validate stdout stream
    await expectLater(process.stdout, emitsThrough('Processing complete.'));

    // 4. Validate exit code
    await process.shouldExit(0);

    // 5. Validate filesystem mutations
    await d.dir('project', [
      d.file('config.json', '{"key": "value"}'),
      d.file('output.log', 'Success')
    ]).validate();
  });
}

Skills similaires