Skip to content

Commit

Permalink
support linux and osx specific command properties
Browse files Browse the repository at this point in the history
- as per https://github.com/microsoft/vscode/blob/1.35.1/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts#L255, vsCode supports having separated command properties for Windows, OSX, and Linux. This change adds the same support to Theia.
- changed the `command` property in CommandProperties interface from mandatory to optional.
- part of #5516

Signed-off-by: elaihau <[email protected]>
  • Loading branch information
elaihau authored and elaihau committed Jul 2, 2019
1 parent 756e440 commit 65ffea5
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 103 deletions.
32 changes: 25 additions & 7 deletions packages/task/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ Each task configuration looks like this:
"-alR"
],
"options": {
"cwd": "${workspaceFolder}",

"cwd": "${workspaceFolder}"
},
"windows": {
"command": "cmd.exe",
Expand All @@ -41,8 +40,13 @@ Each task configuration looks like this:
- *env*: the environment of the executed program or shell. If omitted the parent process' environment is used.
- *shell*: configuration of the shell when task type is `shell`, where users can specify the shell to use with *shell*, and the arguments to be passed to the shell executable to run in command mode with *args*.

By default, *command* and *args* above are used on all platforms. However it's not always possible to express a task in the same way, both on Unix and Windows. The command and/or arguments may be different, for example. If a task needs to work on Linux, MacOS, and Windows, it is better to have separated command, command arguments, and options.

*windows*: if *windows* is defined, its command, command arguments, and options (i.e., *windows.command*, *windows.args*, and *windows.options*) will take precedence over the *command*, *args*, and *options*, when the task is executed on a Windows backend.

*osx*: if *osx* is defined, its command, command arguments, and options (i.e., *osx.command*, *osx.args*, and *osx.options*) will take precedence over the *command*, *args*, and *options*, when the task is executed on a MacOS backend.

*windows*: by default, *command* and *args* above are used on all platforms. However it's not always possible to express a task in the same way, both on Unix and Windows. The command and/or arguments may be different, for example. If a task needs to work on both Linux/MacOS and Windows, it can be better to have two separate process options. If *windows* is defined, it will be used instead of *command* and *args*, when a task is executed on a Windows backend.
*linux*: if *linux* is defined, its command, command arguments, and options (i.e., *linux.command*, *linux.args*, and *linux.options*) will take precedence over the *command*, *args*, and *options*, when the task is executed on a Linux backend.

Here is a sample tasks.json that can be used to test tasks. Just add this content under the theia source directory, in directory `.theia`:
``` json
Expand All @@ -54,9 +58,9 @@ Here is a sample tasks.json that can be used to test tasks. Just add this conten
"type": "shell",
"command": "./task",
"args": [
"1",
"2",
"3"
"default 1",
"default 2",
"default 3"
],
"options": {
"cwd": "${workspaceFolder}/packages/task/src/node/test-resources/"
Expand All @@ -66,7 +70,14 @@ Here is a sample tasks.json that can be used to test tasks. Just add this conten
"args": [
"/c",
"task.bat",
"abc"
"windows abc"
]
},
"linux": {
"args": [
"linux 1",
"linux 2",
"linux 3"
]
}
},
Expand Down Expand Up @@ -128,6 +139,13 @@ The variables are supported in the following properties, using `${variableName}`
- `options.cwd`
- `windows.command`
- `windows.args`
- `windows.options.cwd`
- `osx.command`
- `osx.args`
- `osx.options.cwd`
- `linux.command`
- `linux.args`
- `linux.options.cwd`

See [here](https://www.theia-ide.org/doc/index.html) for a detailed documentation.

Expand Down
10 changes: 10 additions & 0 deletions packages/task/src/browser/process/process-task-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ export class ProcessTaskResolver implements TaskResolver {
args: processTaskConfig.windows.args ? await this.variableResolverService.resolveArray(processTaskConfig.windows.args, variableResolverOptions) : undefined,
options: processTaskConfig.windows.options
} : undefined,
osx: processTaskConfig.osx ? {
command: await this.variableResolverService.resolve(processTaskConfig.osx.command, variableResolverOptions),
args: processTaskConfig.osx.args ? await this.variableResolverService.resolveArray(processTaskConfig.osx.args, variableResolverOptions) : undefined,
options: processTaskConfig.osx.options
} : undefined,
linux: processTaskConfig.linux ? {
command: await this.variableResolverService.resolve(processTaskConfig.linux.command, variableResolverOptions),
args: processTaskConfig.linux.args ? await this.variableResolverService.resolveArray(processTaskConfig.linux.args, variableResolverOptions) : undefined,
options: processTaskConfig.linux.options
} : undefined,
options: {
cwd: await this.variableResolverService.resolve(processTaskConfig.options && processTaskConfig.options.cwd || '${workspaceFolder}', variableResolverOptions),
env: processTaskConfig.options && processTaskConfig.options.env,
Expand Down
121 changes: 68 additions & 53 deletions packages/task/src/browser/task-schema-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,57 @@ export class TaskSchemaUpdater {
}
}

const commandSchema: IJSONSchema = {
type: 'string',
description: 'The actual command or script to execute'
};

const commandArgSchema: IJSONSchema = {
type: 'array',
description: 'A list of strings, each one being one argument to pass to the command',
items: {
type: 'string'
}
};

const commandOptionsSchema: IJSONSchema = {
type: 'object',
description: 'The command options used when the command is executed',
properties: {
cwd: {
type: 'string',
description: 'The directory in which the command will be executed',
default: '${workspaceFolder}'
},
env: {
type: 'object',
description: 'The environment of the executed program or shell. If omitted the parent process\' environment is used'
},
shell: {
type: 'object',
description: 'Configuration of the shell when task type is `shell`',
properties: {
executable: {
type: 'string',
description: 'The shell to use'
},
args: {
type: 'array',
description: `The arguments to be passed to the shell executable to run in command mode
(e.g ['-c'] for bash or ['/S', '/C'] for cmd.exe)`,
items: {
type: 'string'
}
}
}
}
}
};

const taskConfigurationSchema: IJSONSchema = {
oneOf: [
{
'allOf': [
allOf: [
{
type: 'object',
required: ['type', 'label'],
Expand All @@ -77,68 +124,36 @@ const taskConfigurationSchema: IJSONSchema = {
default: 'shell',
description: 'Determines what type of process will be used to execute the task. Only shell types will have output shown on the user interface'
},
command: {
type: 'string',
description: 'The actual command or script to execute'
},
args: {
type: 'array',
description: 'A list of strings, each one being one argument to pass to the command',
items: {
type: 'string'
command: commandSchema,
args: commandArgSchema,
options: commandOptionsSchema,
windows: {
type: 'object',
description: 'Windows specific command configuration that overrides the command, args, and options',
properties: {
command: commandSchema,
args: commandArgSchema,
options: commandOptionsSchema
}
},
options: {
osx: {
type: 'object',
description: 'The command options used when the command is executed',
description: 'MacOS specific command configuration that overrides the command, args, and options',
properties: {
cwd: {
type: 'string',
description: 'The directory in which the command will be executed',
default: '${workspaceFolder}'
},
env: {
type: 'object',
description: 'The environment of the executed program or shell. If omitted the parent process\' environment is used'
},
shell: {
type: 'object',
description: 'Configuration of the shell when task type is `shell`',
properties: {
executable: {
type: 'string',
description: 'The shell to use'
},
args: {
type: 'array',
description: `The arguments to be passed to the shell executable to run in command mode
(e.g ['-c'] for bash or ['/S', '/C'] for cmd.exe)`,
items: {
type: 'string'
}
}
}
}
command: commandSchema,
args: commandArgSchema,
options: commandOptionsSchema
}
},
windows: {
linux: {
type: 'object',
description: 'Windows specific command configuration overrides command and args',
description: 'Linux specific command configuration that overrides the default command, args, and options',
properties: {
command: {
type: 'string',
description: 'The actual command or script to execute'
},
args: {
type: 'array',
description: 'A list of strings, each one being one argument to pass to the command',
items: {
type: 'string'
}
},
command: commandSchema,
args: commandArgSchema,
options: commandOptionsSchema
}
}

}
}
]
Expand Down
14 changes: 12 additions & 2 deletions packages/task/src/common/process/task-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export interface CommandOptions {
}

export interface CommandProperties<T = string> {
readonly command: string;
readonly command?: string;
readonly args?: T[];
readonly options?: CommandOptions;
}
Expand All @@ -62,9 +62,19 @@ export interface ProcessTaskConfiguration<T = string> extends TaskConfiguration,
readonly type: ProcessType;

/**
* Windows version of CommandProperties. Used in preference on Windows, if defined.
* Windows specific task configuration
*/
readonly windows?: CommandProperties<T>;

/**
* macOS specific task configuration
*/
readonly osx?: CommandProperties<T>;

/**
* Linux specific task configuration
*/
readonly linux?: CommandProperties<T>;
}

export interface ProcessTaskInfo extends TaskInfo {
Expand Down
101 changes: 68 additions & 33 deletions packages/task/src/node/process/process-task-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
********************************************************************************/

import { injectable, inject, named } from 'inversify';
import { isWindows, ILogger } from '@theia/core';
import { isWindows, isOSX, ILogger } from '@theia/core';
import { FileUri } from '@theia/core/lib/node';
import {
TerminalProcessOptions,
Expand Down Expand Up @@ -59,39 +59,9 @@ export class ProcessTaskRunner implements TaskRunner {
throw new Error("Process task config must have 'command' property specified");
}

let command: string | undefined;
let args: Array<string | QuotedString> | undefined;
let options: CommandOptions = {};

// on windows, windows-specific options, if available, takes precedence
if (isWindows && taskConfig.windows !== undefined) {
if (taskConfig.windows.command) {
command = taskConfig.windows.command;
args = taskConfig.windows.args;
options = taskConfig.windows.options;
}
} else {
command = taskConfig.command;
args = taskConfig.args;
options = taskConfig.options;
}

// sanity checks:
// - we expect the cwd to be set by the client.
if (!options || !options.cwd) {
throw new Error("Can't run a task when 'cwd' is not provided by the client");
}

// Use task's cwd with spawned process and pass node env object to
// new process, so e.g. we can re-use the system path
if (options) {
options.env = {
...process.env,
...(options.env || {})
};
}

try {
const { command, args, options } = this.getResolvedCommand(taskConfig);

const processType = taskConfig.type === 'process' ? 'process' : 'shell';
let proc: Process;

Expand Down Expand Up @@ -139,6 +109,71 @@ export class ProcessTaskRunner implements TaskRunner {
}
}

private getResolvedCommand(taskConfig: TaskConfiguration): {
command: string | undefined,
args: Array<string | QuotedString> | undefined,
options: CommandOptions
} {
let systemSpecificCommand: {
command: string | undefined,
args: Array<string | QuotedString> | undefined,
options: CommandOptions
};
// on windows, windows-specific options, if available, take precedence
if (isWindows && taskConfig.windows !== undefined) {
systemSpecificCommand = this.getSystemSpecificCommand(taskConfig, 'windows');
} else if (isOSX && taskConfig.osx !== undefined) { // on macOS, mac-specific options, if available, take precedence
systemSpecificCommand = this.getSystemSpecificCommand(taskConfig, 'osx');
} else if (!isWindows && !isOSX && taskConfig.linux !== undefined) { // on linux, linux-specific options, if available, take precedence
systemSpecificCommand = this.getSystemSpecificCommand(taskConfig, 'linux');
} else { // system-specific options are unavailable, use the default
systemSpecificCommand = this.getSystemSpecificCommand(taskConfig, undefined);
}

const options = systemSpecificCommand.options;
// sanity checks:
// - we expect the cwd to be set by the client.
if (!options || !options.cwd) {
throw new Error("Can't run a task when 'cwd' is not provided by the client");
}

// Use task's cwd with spawned process and pass node env object to
// new process, so e.g. we can re-use the system path
if (options) {
options.env = {
...process.env,
...(options.env || {})
};
}

return systemSpecificCommand;
}

private getSystemSpecificCommand(taskConfig: TaskConfiguration, system: 'windows' | 'linux' | 'osx' | undefined): {
command: string | undefined,
args: Array<string | QuotedString> | undefined,
options: CommandOptions
} {
// initialise with default values from the `taskConfig`
let command: string | undefined = taskConfig.command;
let args: Array<string | QuotedString> | undefined = taskConfig.args;
let options: CommandOptions = taskConfig.options || {};

if (system) {
if (taskConfig[system].command) {
command = taskConfig[system].command;
}
if (taskConfig[system].args) {
args = taskConfig[system].args;
}
if (taskConfig[system].options) {
options = taskConfig[system].options;
}
}

return { command, args, options };
}

protected asFsPath(uriOrPath: string) {
return (uriOrPath.startsWith('file:/'))
? FileUri.fsPath(uriOrPath)
Expand Down
Loading

0 comments on commit 65ffea5

Please sign in to comment.