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: run commands in single shell #369

Merged
merged 1 commit into from
Sep 7, 2022
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
23 changes: 5 additions & 18 deletions packages/melos/lib/src/commands/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -173,37 +173,24 @@ mixin _BootstrapMixin on _CleanMixin {
Package package, {
bool inTemporaryProject = false,
}) async {
final execArgs = [
final command = [
...pubCommandExecArgs(
useFlutter: package.isFlutterPackage,
workspace: workspace,
),
'get',
if (workspace.config.commands.bootstrap.runPubGetOffline) '--offline'
];
].join(' ');

final executable = currentPlatform.isWindows ? 'cmd' : '/bin/sh';
final packagePath = inTemporaryProject
? join(workspace.melosToolPath, package.pathRelativeToWorkspace)
: package.path;
final process = await Process.start(
executable,
currentPlatform.isWindows ? ['/C', '%MELOS_SCRIPT%'] : [],

final process = await startCommandRaw(
command,
workingDirectory: packagePath,
environment: {
utils.envKeyMelosTerminalWidth: utils.terminalWidth.toString(),
'MELOS_SCRIPT': execArgs.join(' '),
},
runInShell: true,
);

if (!currentPlatform.isWindows) {
// Pipe in the arguments to trigger the script to run.
process.stdin.writeln(execArgs.join(' '));
// Exit the process with the same exit code as the previous command.
process.stdin.writeln(r'exit $?');
}

const logTimeout = Duration(seconds: 10);
final packagePrefix = '[${AnsiStyles.blue.bold(package.name)}]: ';
void Function(String) logLineTo(void Function(String) log) =>
Expand Down
2 changes: 1 addition & 1 deletion packages/melos/lib/src/commands/exec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ mixin _ExecMixin on _Melos {
environment.remove(envKeyMelosTerminalWidth);
}

return startProcess(
return startCommand(
execArgs,
logger: logger,
environment: environment,
Expand Down
2 changes: 1 addition & 1 deletion packages/melos/lib/src/commands/run.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ mixin _RunMixin on _Melos {
.child(runningLabel)
.newLine();

return startProcess(
return startCommand(
scriptParts..addAll(extraArgs),
logger: logger,
environment: environment,
Expand Down
101 changes: 59 additions & 42 deletions packages/melos/lib/src/common/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -277,59 +277,76 @@ bool isWorkspaceDirectory(String directory) =>
bool isPackageDirectory(String directory) =>
fileExists(pubspecPathForDirectory(directory));

Future<int> startProcess(
List<String> execArgs, {
String? prefix,
Map<String, String> environment = const {},
Future<Process> startCommandRaw(
String command, {
String? workingDirectory,
bool onlyOutputOnError = false,
Map<String, String> environment = const {},
bool includeParentEnvironment = true,
required MelosLogger logger,
}) async {
final workingDirectoryPath = workingDirectory ?? Directory.current.path;
final executable = currentPlatform.isWindows ? 'cmd' : '/bin/sh';
final filteredArgs = execArgs.map((arg) {
// Remove empty args.
if (arg.trim().isEmpty) {
return null;
}

// Attempt to make line continuations Windows & Linux compatible.
if (arg.trim() == r'\') {
return currentPlatform.isWindows ? arg.replaceAll(r'\', '^') : arg;
}
if (arg.trim() == '^') {
return currentPlatform.isWindows ? arg : arg.replaceAll('^', r'\');
}

// Inject MELOS_* variables if any.
environment.forEach((key, value) {
if (key.startsWith('MELOS_')) {
arg = arg.replaceAll('\$$key', value);
arg = arg.replaceAll(key, value);
}
});

return arg;
}).where((element) => element != null);
}) {
final executable = currentPlatform.isWindows ? 'cmd.exe' : '/bin/sh';
workingDirectory ??= Directory.current.path;

final execProcess = await Process.start(
return Process.start(
executable,
currentPlatform.isWindows
? ['/C', '%MELOS_SCRIPT%']
: ['-c', r'eval "$MELOS_SCRIPT"'],
workingDirectory: workingDirectoryPath,
workingDirectory: workingDirectory,
environment: {
...environment,
envKeyMelosTerminalWidth: terminalWidth.toString(),
'MELOS_SCRIPT': filteredArgs.join(' '),
'MELOS_SCRIPT': command,
},
includeParentEnvironment: includeParentEnvironment,
runInShell: true,
);
}

Future<int> startCommand(
List<String> command, {
String? prefix,
Map<String, String> environment = const {},
String? workingDirectory,
bool onlyOutputOnError = false,
bool includeParentEnvironment = true,
required MelosLogger logger,
}) async {
final processedCommand = command
.map((arg) {
// Remove empty args.
if (arg.trim().isEmpty) {
return null;
}

// Attempt to make line continuations Windows & Linux compatible.
if (arg.trim() == r'\') {
return currentPlatform.isWindows ? arg.replaceAll(r'\', '^') : arg;
}
if (arg.trim() == '^') {
return currentPlatform.isWindows ? arg : arg.replaceAll('^', r'\');
}

// Inject MELOS_* variables if any.
environment.forEach((key, value) {
if (key.startsWith('MELOS_')) {
arg = arg.replaceAll('\$$key', value);
arg = arg.replaceAll(key, value);
}
});

return arg;
})
.where((element) => element != null)
.join(' ');

final process = await startCommandRaw(
processedCommand,
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
);

var stdoutStream = execProcess.stdout;
var stderrStream = execProcess.stderr;
var stdoutStream = process.stdout;
var stderrStream = process.stderr;

if (prefix != null && prefix.isNotEmpty) {
final pluginPrefixTransformer =
Expand All @@ -344,12 +361,12 @@ Future<int> startProcess(
},
);

stdoutStream = execProcess.stdout
stdoutStream = process.stdout
.transform<String>(utf8.decoder)
.transform<String>(pluginPrefixTransformer)
.transform<List<int>>(utf8.encoder);

stderrStream = execProcess.stderr
stderrStream = process.stderr
.transform<String>(utf8.decoder)
.transform<String>(pluginPrefixTransformer)
.transform<List<int>>(utf8.encoder);
Expand Down Expand Up @@ -381,7 +398,7 @@ Future<int> startProcess(

await processStdoutCompleter.future;
await processStderrCompleter.future;
final exitCode = await execProcess.exitCode;
final exitCode = await process.exitCode;

if (onlyOutputOnError && exitCode > 0) {
logger.stdout(utf8.decode(processStdout, allowMalformed: true));
Expand Down
4 changes: 2 additions & 2 deletions packages/melos/lib/src/workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class MelosWorkspace {
if (childProcessPath != null) 'PATH': childProcessPath!,
};

return utils.startProcess(
return utils.startCommand(
execArgs,
logger: logger,
environment: environment,
Expand All @@ -203,7 +203,7 @@ class MelosWorkspace {
if (childProcessPath != null) 'PATH': childProcessPath!,
};

return utils.startProcess(
return utils.startCommand(
execArgs,
logger: logger,
environment: environment,
Expand Down
29 changes: 29 additions & 0 deletions packages/melos/test/utils_test.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import 'dart:io';

import 'package:melos/src/common/io.dart';
import 'package:melos/src/common/utils.dart';
import 'package:melos/src/logging.dart';
import 'package:path/path.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
import 'package:test/test.dart';

import 'matchers.dart';
import 'utils.dart';

void main() {
Expand Down Expand Up @@ -72,4 +75,30 @@ void main() {
);
});
});

group('startProcess', () {
test('runs command chain in single shell', () async {
final workspaceDir = createTemporaryWorkspaceDirectory();
final testDir = p.join(workspaceDir.path, 'test');

ensureDir(testDir);

final logger = TestLogger();
await startCommand(
[
'cd',
'test',
'&&',
if (Platform.isWindows) 'cd' else 'pwd',
],
logger: logger.toMelosLogger(),
workingDirectory: workspaceDir.path,
);

expect(
logger.output.normalizeNewLines(),
ignoringAnsii('$testDir\n'),
);
});
});
}