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(dotnet): should work with paths that contain spaces #392

Merged
merged 9 commits into from
Mar 3, 2022
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@

<p align="center">
<image src="https://user-images.githubusercontent.com/6933928/135131740-66939491-dc3e-4982-82ac-e6584530bb1b.png" alt="nx-dotnet logo"/>
</p>

# NxDotnet

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->

[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-)

<!-- ALL-CONTRIBUTORS-BADGE:END -->

[![Join the chat at https://gitter.im/nx-dotnet-plugin/community](https://badges.gitter.im/nx-dotnet-plugin/community.svg)](https://gitter.im/nx-dotnet-plugin/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Run CI checks](https://github.com/nx-dotnet/nx-dotnet/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/nx-dotnet/nx-dotnet/actions/workflows/main.yml)
Expand Down
10 changes: 5 additions & 5 deletions docs/core/guides/handling-solutions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ To add projects to a solution file by default, you can set the generator default
```json5
{
// ... more nx.json configuration
"generators": {
generators: {
// ... other default configurations
"@nx-dotnet/core:application": {
"solutionFile": true,
'@nx-dotnet/core:application': {
solutionFile: true,
},
// ... other default configurations
"@nx-dotnet/core:library" : {
"solutionFile": "my-sln.sln",
'@nx-dotnet/core:library': {
solutionFile: 'my-sln.sln',
},
},
}
Expand Down
152 changes: 76 additions & 76 deletions packages/dotnet/src/lib/core/dotnet.client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ChildProcess, execSync, spawn } from 'child_process';

import { getParameterString, swapKeysUsingMap } from '@nx-dotnet/utils';
import { getParameterArrayStrings, swapKeysUsingMap } from '@nx-dotnet/utils';
import { ChildProcess, spawn, spawnSync } from 'child_process';

import {
addPackageKeyMap,
Expand All @@ -24,160 +23,161 @@ import { LoadedCLI } from './dotnet.factory';
export class DotNetClient {
constructor(private cliCommand: LoadedCLI, public cwd?: string) {}

new(template: dotnetTemplate, parameters?: dotnetNewOptions): void {
let cmd = `${this.cliCommand.command} new ${template}`;
new(template: dotnetTemplate, parameters?: dotnetNewOptions): Buffer {
const params = [`new`, template];
if (parameters) {
parameters = swapKeysUsingMap(parameters, newKeyMap);
const paramString = parameters ? getParameterString(parameters) : '';
cmd = `${cmd} ${paramString}`;
params.push(...getParameterArrayStrings(parameters));
}
return this.logAndExecute(cmd);
return this.logAndExecute(params);
}

build(project: string, parameters?: dotnetBuildOptions): void {
let cmd = `${this.cliCommand.command} build "${project}"`;
build(project: string, parameters?: dotnetBuildOptions): Buffer {
const params = [`build`, project];
if (parameters) {
parameters = swapKeysUsingMap(parameters, buildKeyMap);
const paramString = parameters ? getParameterString(parameters) : '';
cmd = `${cmd} ${paramString}`;
params.push(...getParameterArrayStrings(parameters));
}
return this.logAndExecute(cmd);
return this.logAndExecute(params);
}

run(
project: string,
watch = false,
parameters?: dotnetRunOptions,
): ChildProcess {
let cmd = watch
? `watch --project "${project}" run`
: `run --project "${project}"`;
const params = watch
? [`watch`, `--project`, project, `run`]
: [`run`, `--project`, project];
if (parameters) {
parameters = swapKeysUsingMap(parameters, runKeyMap);
const paramString = parameters ? getParameterString(parameters) : '';
cmd = `${cmd} ${paramString}`;
params.push(...getParameterArrayStrings(parameters));
}
console.log(`Executing Command: dotnet ${cmd}`);
return spawn(this.cliCommand.command, cmd.split(' '), {
stdio: 'inherit',
cwd: this.cwd,
});

return this.logAndSpawn(params);
}

test(
project: string,
watch?: boolean,
parameters?: dotnetTestOptions,
): void | ChildProcess {
let cmd = watch ? `watch --project "${project}" test` : `test "${project}"`;
cmd = `${this.cliCommand.command} ${cmd}`;
): Buffer | ChildProcess {
const params = watch
? [`watch`, `--project`, project, `test`]
: [`test`, project];

if (parameters) {
const mappedParameters = swapKeysUsingMap(parameters, testKeyMap);
const paramString = getParameterString(mappedParameters);
cmd = `${cmd} ${paramString}`;
parameters = swapKeysUsingMap(parameters, testKeyMap);
params.push(...getParameterArrayStrings(parameters));
}
if (!watch) {
return this.logAndExecute(cmd);
return this.logAndExecute(params);
} else {
console.log(`Executing Command: ${cmd}`);
const params = cmd
.split(' ')
.slice(1)
.filter((x) => x.length);
return spawn(this.cliCommand.command, params, {
stdio: 'inherit',
cwd: this.cwd,
});
const slicedParams = params.slice(1).filter((x) => x.length);
return this.logAndSpawn(slicedParams);
}
}

addPackageReference(
project: string,
pkg: string,
parameters?: dotnetAddPackageOptions,
): void {
let cmd = `${this.cliCommand.command} add "${project}" package ${pkg}`;
): Buffer {
const params = [`add`, project, `package`, pkg];
if (parameters) {
parameters = swapKeysUsingMap(parameters, addPackageKeyMap);
const paramString = parameters ? getParameterString(parameters) : '';
cmd = `${cmd} ${paramString}`;
params.push(...getParameterArrayStrings(parameters));
}
return this.logAndExecute(cmd);
return this.logAndExecute(params);
}

addProjectReference(hostCsProj: string, targetCsProj: string): void {
return this.logAndExecute(
`${this.cliCommand.command} add ${hostCsProj} reference ${targetCsProj}`,
);
addProjectReference(hostCsProj: string, targetCsProj: string): Buffer {
return this.logAndExecute([`add`, hostCsProj, `reference`, targetCsProj]);
}

publish(
project: string,
parameters?: dotnetPublishOptions,
publishProfile?: string,
extraParameters?: string,
): void {
let cmd = `${this.cliCommand.command} publish "${project}"`;
): Buffer {
const params = [`publish`, `"${project}"`];
if (parameters) {
parameters = swapKeysUsingMap(parameters, publishKeyMap);
const paramString = parameters ? getParameterString(parameters) : '';
cmd = `${cmd} ${paramString}`;
params.push(...getParameterArrayStrings(parameters));
}
if (publishProfile) {
cmd = `${cmd} -p:PublishProfile=${publishProfile}`;
params.push(`-p:PublishProfile=${publishProfile}`);
}
if (extraParameters) {
cmd = `${cmd} ${extraParameters}`;
params.push(`${extraParameters}`);
}
return this.logAndExecute(cmd);
return this.logAndExecute(params);
}

installTool(tool: string): void {
const cmd = `${this.cliCommand.command} tool install ${tool}`;
installTool(tool: string): Buffer {
const cmd = [`tool`, `install`, tool];
return this.logAndExecute(cmd);
}

restorePackages(project: string): void {
const cmd = `${this.cliCommand.command} restore "${project}"`;
restorePackages(project: string): Buffer {
const cmd = [`restore`, project];
return this.logAndExecute(cmd);
}

restoreTools(): void {
const cmd = `${this.cliCommand.command} tool restore`;
restoreTools(): Buffer {
const cmd = [`tool`, `restore`];
return this.logAndExecute(cmd);
}

format(project: string, parameters?: dotnetFormatOptions): void {
let cmd = `${this.cliCommand.command} format "${project}"`;
format(project: string, parameters?: dotnetFormatOptions): Buffer {
const params = [`format`, project];
if (parameters) {
parameters = swapKeysUsingMap(parameters, formatKeyMap);
const paramString = parameters ? getParameterString(parameters) : '';
cmd = `${cmd} ${paramString}`;
params.push(...getParameterArrayStrings(parameters));
}
return this.logAndExecute(cmd);
return this.logAndExecute(params);
}

addProjectToSolution(solutionFile: string, project: string) {
const cmd = `${this.cliCommand.command} sln "${solutionFile}" add "${project}"`;
this.logAndExecute(cmd);
const params = [`sln`, solutionFile, `add`, project];
this.logAndExecute(params);
}

getSdkVersion(): Buffer {
const cmd = 'dotnet --version';
return this.execute(cmd);
return this.execute(['--version']);
}

printSdkVersion(): void {
this.logAndExecute('dotnet --version');
this.logAndExecute(['--version']);
}

private logAndExecute(cmd: string): void {
console.log(`Executing Command: ${cmd}`);
execSync(cmd, { stdio: 'inherit', cwd: this.cwd || process.cwd() });
private logAndExecute(params: string[]): Buffer {
console.log(
`Executing Command: ${this.cliCommand.command} ${params.join(', ')}`,
);
return this.execute(params);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method needs the 'stdio: inherit' so that things like nx build show terminal output.

}

private execute(cmd: string): Buffer {
return execSync(cmd, { cwd: this.cwd || process.cwd() });
private execute(params: string[]): Buffer {
return spawnSync(this.cliCommand.command, params, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we swapping to spawnSync over execSync here? This is more complex maintainability-wise and shouldn't be needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not totally opposed to spawnSync actually, some stuff does work out better this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, had a lengthy meeting couldn't look into this.

I converted to spawnSync to avoid converting all parameters to string first, spawnSync let us pass each parameter on the array so it is automatically escaped.
With execSync I had some problems passing on "Security Hotspot" tests because it allowed the user to pass arbitrary commands to execSync, like appending in the last parameter: "&& rm -rf *" this would delete everything on the current folder. Using the spawnSync command prevents the user to do this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So with nx-plugins and other dev tooling in general, the hotspots that pop up from sonarqube can be mostly ignored. The reason being that those commands are being run by the developer, on their own machine. It's not really an attack point, as someone would already have to be able to run arbitrary commands to exercise it.

The solution for most things that come up like that, are just for me to hit the "not a problem" button while reviewing the PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be a lot easier because I had to change the method a lot, I'll take it into account next time, for this one I was kinda in a rush to have a deployable version because the previous one was broken.

stdio: 'inherit',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding 'inherit' here causes the returned buffers to be empty

Copy link
Contributor Author

@asinino asinino Mar 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a new solution for this have to discuss with you, I didn't merge it yet. This should resolve the problem:

  private execute(params: string[], stdio: StdioOptions = 'pipe'): Buffer {
    const proc = spawnSync(this.cliCommand.command, params, {
      stdio,
      cwd: this.cwd || process.cwd(),
    });

    let result = Buffer.alloc(0);
    if (proc?.stdout) {
      result = Buffer.concat([result, proc.stdout]);
    }
    if (proc?.stderr) {
      result = Buffer.concat([result, proc.stderr]);
    }
    return result;
  }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would work, but its fine to duplicate the spawnSync call for this. The idea behind execute is that we are doing some kind of processing to the Buffer on our side, and we need it. If we are displaying the outputs of the command to the user, its the wrong method to use.

Copy link
Contributor Author

@asinino asinino Mar 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with different calls for spawnSync with different behavior, it's easier to not mess it up.

cwd: this.cwd || process.cwd(),
})
.output.filter((buf) => buf !== null)
.reduce(
(acc, buf) => Buffer.concat([acc as Buffer, buf as Buffer]),
Buffer.from(''),
) as Buffer;
}

private logAndSpawn(params: string[]): ChildProcess {
console.log(
`Executing Command: ${this.cliCommand.command} ${params.join(', ')}`,
);
return spawn(this.cliCommand.command, params, {
stdio: 'inherit',
cwd: this.cwd || process.cwd(),
});
}
}
21 changes: 21 additions & 0 deletions packages/utils/src/lib/utility-functions/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,24 @@ export function getParameterString(
}
}, '');
}

/**
* Transforms an object of parameters into an array of strings with key followed by the corresponding value.
* @param parameters Parameters to transform into CLI arguments
* @returns Parameter string to be appended to CLI command
*/
export function getParameterArrayStrings(
parameters: Record<string, boolean | string>,
): string[] {
return Object.entries(parameters).reduce((acc, [flag, value]) => {
if (typeof value === 'boolean' || !value) {
if (value) {
return [...acc, `--${flag}`];
} else {
return acc;
}
} else {
return [...acc, `--${flag}`, value.toString()];
}
}, new Array<string>());
}