Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: maintain working directory across script steps #711

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 12 additions & 35 deletions packages/melos/lib/src/commands/run.dart
Original file line number Diff line number Diff line change
Expand Up @@ -275,48 +275,25 @@ mixin _RunMixin on _Melos {
Script script,
Map<String, String> 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<void> _executeAndLogCommand(
Script script,
String scriptSourceCode,
String scriptCommand,
Map<String, String> 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,
jessicatarra marked this conversation as resolved.
Show resolved Hide resolved
workingDirectory: config.path,
environment: environment,
);

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();
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/melos/lib/src/commands/runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
108 changes: 108 additions & 0 deletions packages/melos/lib/src/common/persistent_shell.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import '../logging.dart';
import 'environment_variable_key.dart';
import 'platform.dart';
import 'utils.dart';

class PersistentShell {
PersistentShell({
required this.logger,
required this.environment,
this.workingDirectory,
});

final _isWindows = currentPlatform.isWindows;
final MelosLogger logger;
final Map<String, String> environment;
final String? workingDirectory;
late final Process _process;
Completer<void>? _commandCompleter;
final String _successEndMarker = '__SUCCESS_COMMAND_END__';
final String _failureEndMarker = '__FAILURE_COMMAND_END__';

Future<void> startShell() async {
final executable = _isWindows ? 'cmd.exe' : '/bin/sh';

_process = await Process.start(
executable,
[],
workingDirectory: workingDirectory,
environment: {
...environment,
EnvironmentVariableKey.melosTerminalWidth: terminalWidth.toString(),
},
);

_listenToProcessStream(_process.stdout);
_listenToProcessStream(_process.stderr, isError: true);
}

Future<bool> sendCommand(String command) async {
assert(_commandCompleter == null, 'A command is already in progress.');
_commandCompleter = Completer<void>();

final fullCommand = _buildFullCommand(command);
_process.stdin.writeln(fullCommand);

return _awaitCommandCompletion();
}

Future<void> stopShell() async {
await _process.stdin.close();
final exitCode = await _process.exitCode;
if (exitCode == 0) {
logger.log(successLabel);
return;
}
logger.log(failedLabel);
}

Future<bool> _awaitCommandCompletion() async {
try {
await _commandCompleter!.future;
return true;
} catch (e) {
return false;
} finally {
_commandCompleter = null;
}
}

void _listenToProcessStream(
Stream<List<int>> 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
''';
}
}
4 changes: 4 additions & 0 deletions packages/melos/lib/src/common/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
44 changes: 44 additions & 0 deletions packages/melos/lib/src/logging.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:ansi_styles/ansi_styles.dart';
import 'package:cli_util/cli_logging.dart';

Expand Down Expand Up @@ -67,6 +69,48 @@ class MelosLogger with _DelegateLogger {
write(message);
}

void logAndCompleteBasedOnMarkers(
String message,
String successMarker,
String failureMarker,
Completer<void>? completer, {
bool isError = false,
}) {
final modifiedMessage = _processMessageBasedOnMarkers(
message,
successMarker,
failureMarker,
completer,
);
_logMessage(modifiedMessage, isError);
}

String _processMessageBasedOnMarkers(
String message,
String successMarker,
String failureMarker,
Completer<void>? 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)}');
Expand Down
Loading
Loading