diff --git a/packages/melos/lib/src/commands/run.dart b/packages/melos/lib/src/commands/run.dart index d31fb8d0..8501bc6b 100644 --- a/packages/melos/lib/src/commands/run.dart +++ b/packages/melos/lib/src/commands/run.dart @@ -275,48 +275,24 @@ mixin _RunMixin on _Melos { Script script, Map environment, ) async { - for (final step in steps) { - final scriptCommand = _buildScriptCommand(step, scripts); - - final scriptSourceCode = targetStyle( - step.withoutTrailing('\n'), - ); - - await _executeAndLogCommand( - script, - scriptSourceCode, - scriptCommand, - environment, - ); - } - } - - Future _executeAndLogCommand( - Script script, - String scriptSourceCode, - String scriptCommand, - Map environment, - ) async { - logger.command('melos run ${script.name}'); - logger.child(scriptSourceCode).child(runningLabel).newLine(); - - final exitCode = await startCommand( - [scriptCommand], + final shell = PersistentShell( logger: logger, - environment: environment, workingDirectory: config.path, ); - logger.newLine(); + await shell.startShell(); logger.command('melos run ${script.name}'); - final resultLogger = logger.child(scriptSourceCode); - if (exitCode != 0) { - resultLogger.child(failedLabel); - } else { - resultLogger.child(successLabel); + for (final step in steps) { + final scriptCommand = _buildScriptCommand(step, scripts); + + final shouldContinue = await shell.sendCommand(scriptCommand); + if (!shouldContinue) { + break; + } } - logger.newLine(); + + await shell.stopShell(); } } diff --git a/packages/melos/lib/src/commands/runner.dart b/packages/melos/lib/src/commands/runner.dart index 6f7f7e5c..465233bf 100644 --- a/packages/melos/lib/src/commands/runner.dart +++ b/packages/melos/lib/src/commands/runner.dart @@ -29,6 +29,7 @@ import '../common/glob.dart'; import '../common/intellij_project.dart'; import '../common/io.dart'; import '../common/pending_package_update.dart'; +import '../common/persistent_shell.dart'; import '../common/platform.dart'; import '../common/utils.dart' as utils; import '../common/utils.dart'; diff --git a/packages/melos/lib/src/common/persistent_shell.dart b/packages/melos/lib/src/common/persistent_shell.dart new file mode 100644 index 00000000..51c884a6 --- /dev/null +++ b/packages/melos/lib/src/common/persistent_shell.dart @@ -0,0 +1,101 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import '../logging.dart'; +import 'platform.dart'; +import 'utils.dart'; + +class PersistentShell { + PersistentShell({ + required this.logger, + this.workingDirectory, + }); + + final _isWindows = currentPlatform.isWindows; + final MelosLogger logger; + final String? workingDirectory; + late final Process _process; + Completer? _commandCompleter; + final String _successEndMarker = '__SUCCESS_COMMAND_END__'; + final String _failureEndMarker = '__FAILURE_COMMAND_END__'; + + Future startShell() async { + final executable = _isWindows ? 'cmd.exe' : '/bin/sh'; + + _process = await Process.start( + executable, + [], + workingDirectory: workingDirectory, + ); + + _listenToProcessStream(_process.stdout); + _listenToProcessStream(_process.stderr, isError: true); + } + + Future sendCommand(String command) async { + assert(_commandCompleter == null, 'A command is already in progress.'); + _commandCompleter = Completer(); + + final fullCommand = _buildFullCommand(command); + _process.stdin.writeln(fullCommand); + + return _awaitCommandCompletion(); + } + + Future stopShell() async { + await _process.stdin.close(); + final exitCode = await _process.exitCode; + if (exitCode == 0) { + logger.log(successLabel); + return; + } + logger.log(failedLabel); + } + + Future _awaitCommandCompletion() async { + try { + await _commandCompleter!.future; + return true; + } catch (e) { + return false; + } finally { + _commandCompleter = null; + } + } + + void _listenToProcessStream( + Stream> stream, { + bool isError = false, + }) { + stream.listen((event) { + final output = utf8.decode(event, allowMalformed: true); + logger.logAndCompleteBasedOnMarkers( + output, + _successEndMarker, + _failureEndMarker, + _commandCompleter, + isError: isError, + ); + }); + } + + String _buildFullCommand(String command) { + final formattedScriptStep = + targetStyle(command.addStepPrefixEmoji().withoutTrailing('\n')); + final echoCommand = 'echo "$formattedScriptStep"'; + final echoSuccess = 'echo $_successEndMarker'; + final echoFailure = 'echo $_failureEndMarker'; + + if (_isWindows) { + return ''' + $echoCommand && $command || VER>NUL && if %ERRORLEVEL% NEQ 0 ($echoFailure) else ($echoSuccess) + '''; + } + + return ''' + $echoCommand && $command || true && if [ \$? -ne 0 ]; + then $echoFailure; else $echoSuccess; fi + '''; + } +} diff --git a/packages/melos/lib/src/common/utils.dart b/packages/melos/lib/src/common/utils.dart index bbc75f37..61547b3e 100644 --- a/packages/melos/lib/src/common/utils.dart +++ b/packages/melos/lib/src/common/utils.dart @@ -73,6 +73,10 @@ final melosPackageUri = Uri.parse('package:melos/melos.dart'); final _camelCasedDelimiterRegExp = RegExp(r'[_\s-]+'); extension StringUtils on String { + String addStepPrefixEmoji() { + return '➡️ step: $this'; + } + String indent(String indent) { final split = this.split('\n'); diff --git a/packages/melos/lib/src/logging.dart b/packages/melos/lib/src/logging.dart index 0f557531..141201c7 100644 --- a/packages/melos/lib/src/logging.dart +++ b/packages/melos/lib/src/logging.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:ansi_styles/ansi_styles.dart'; import 'package:cli_util/cli_logging.dart'; @@ -67,6 +69,48 @@ class MelosLogger with _DelegateLogger { write(message); } + void logAndCompleteBasedOnMarkers( + String message, + String successMarker, + String failureMarker, + Completer? completer, { + bool isError = false, + }) { + final modifiedMessage = _processMessageBasedOnMarkers( + message, + successMarker, + failureMarker, + completer, + ); + _logMessage(modifiedMessage, isError); + } + + String _processMessageBasedOnMarkers( + String message, + String successMarker, + String failureMarker, + Completer? completer, + ) { + if (message.contains(successMarker)) { + completer?.complete(); + return message.replaceAll(successMarker, ''); + } + + if (message.contains(failureMarker)) { + completer?.complete(); + return message.replaceAll(failureMarker, ''); + } + + return message; + } + + void _logMessage(String message, bool isError) { + if (isError) { + error(message); + } + write(message); + } + void command(String command, {bool withDollarSign = false}) { if (withDollarSign) { stdout('${commandColor(r'$')} ${commandStyle(command)}'); diff --git a/packages/melos/test/commands/run_test.dart b/packages/melos/test/commands/run_test.dart index 70242e0f..6b3a94e8 100644 --- a/packages/melos/test/commands/run_test.dart +++ b/packages/melos/test/commands/run_test.dart @@ -437,6 +437,53 @@ melos run test_script group('multiple scripts', () { late Directory aDir; + test( + ''' +Verify that multiple script steps are executed sequentially in a persistent +shell. When the script changes directory to "packages" and runs "ls -la", +it should list the contents including the package named "this is package A". + ''', + () async { + final workspaceDir = await createTemporaryWorkspace( + runPubGet: true, + configBuilder: (path) => MelosWorkspaceConfig( + path: path, + name: 'test_package', + packages: [ + createGlob('packages/**', currentDirectoryPath: path), + ], + scripts: const Scripts({ + 'cd_script': Script( + name: 'cd_script', + steps: ['cd packages', 'ls -la', 'pwd'], + ), + }), + ), + ); + + await createProject( + workspaceDir, + const PubSpec(name: 'this is package A'), + ); + + final logger = TestLogger(); + final config = + await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir); + final melos = Melos( + logger: logger, + config: config, + ); + + await melos.run(scriptName: 'cd_script', noSelect: true); + + expect( + logger.output.normalizeNewLines(), + contains('this is package A'), + ); + }, + timeout: const Timeout(Duration(minutes: 1)), + ); + test( 'verifies that a melos script can successfully call another ' 'script as a step and execute commands', () async { @@ -480,9 +527,7 @@ melos run test_script ignoringDependencyMessages( ''' melos run hello_script - └> test_script - └> RUNNING - +➡️ step: melos run test_script melos run test_script └> echo "test_script" └> RUNNING @@ -493,20 +538,10 @@ melos run test_script └> echo "test_script" └> SUCCESS -melos run hello_script - └> test_script - └> SUCCESS - -melos run hello_script - └> echo "hello world" - └> RUNNING - +➡️ step: echo hello world ${currentPlatform.isWindows ? '"hello world"' : 'hello world'} -melos run hello_script - └> echo "hello world" - └> SUCCESS - +SUCCESS ''', ), ); @@ -601,44 +636,20 @@ melos run hello_script ignoringDependencyMessages( ''' melos run hello_script - └> test_script - └> RUNNING - +➡️ step: melos run test_script melos run test_script - └> echo "test_script_1" - └> RUNNING - +➡️ step: echo test_script_1 ${currentPlatform.isWindows ? '"test_script_1"' : 'test_script_1'} -melos run test_script - └> echo "test_script_1" - └> SUCCESS - -melos run test_script - └> echo "test_script_2" - └> RUNNING - +➡️ step: echo test_script_2 ${currentPlatform.isWindows ? '"test_script_2"' : 'test_script_2'} -melos run test_script - └> echo "test_script_2" - └> SUCCESS - - -melos run hello_script - └> test_script - └> SUCCESS - -melos run hello_script - └> echo "hello world" - └> RUNNING +SUCCESS +➡️ step: echo hello world ${currentPlatform.isWindows ? '"hello world"' : 'hello world'} -melos run hello_script - └> echo "hello world" - └> SUCCESS - +SUCCESS ''', ), ); @@ -707,9 +718,7 @@ melos run hello_script ignoringDependencyMessages( ''' melos run hello_script - └> analyze - └> RUNNING - +➡️ step: melos analyze \$ melos analyze └> dart analyze └> RUNNING (in 3 packages) @@ -739,20 +748,10 @@ c: SUCCESS └> dart analyze └> SUCCESS -melos run hello_script - └> analyze - └> SUCCESS - -melos run hello_script - └> echo "hello world" - └> RUNNING - +➡️ step: echo hello world ${currentPlatform.isWindows ? '"hello world"' : 'hello world'} -melos run hello_script - └> echo "hello world" - └> SUCCESS - +SUCCESS ''', ), ); @@ -814,9 +813,7 @@ melos run hello_script ignoringDependencyMessages( ''' melos run hello_script - └> list - └> RUNNING - +➡️ step: melos run list melos run list └> echo "list script" └> RUNNING @@ -827,20 +824,10 @@ melos run list └> echo "list script" └> SUCCESS -melos run hello_script - └> list - └> SUCCESS - -melos run hello_script - └> echo "hello world" - └> RUNNING - +➡️ step: echo hello world ${currentPlatform.isWindows ? '"hello world"' : 'hello world'} -melos run hello_script - └> echo "hello world" - └> SUCCESS - +SUCCESS ''', ), ); @@ -909,9 +896,7 @@ melos run hello_script ignoringDependencyMessages( ''' melos run hello_script - └> analyze --fatal-infos - └> RUNNING - +➡️ step: melos analyze --fatal-infos \$ melos analyze └> dart analyze --fatal-infos └> RUNNING (in 3 packages) @@ -941,20 +926,10 @@ c: SUCCESS └> FAILED (in 1 packages) └> a (with exit code 1) -melos run hello_script - └> analyze --fatal-infos - └> FAILED - -melos run hello_script - └> echo "hello world" - └> RUNNING - +➡️ step: echo hello world ${currentPlatform.isWindows ? '"hello world"' : 'hello world'} -melos run hello_script - └> echo "hello world" - └> SUCCESS - +SUCCESS ''', ), );