diff --git a/README.md b/README.md index 16321d161..71527c62f 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,16 @@ Run Gradle tasks in VS Code. ## Features -This extension provides a UI layer over Gradle builds. It shows Gradle projects and tasks and allows you to run tasks within the context of the editor. +This extension provides a visual interface for your Gradle build. You can view Gradle projects and run Gradle tasks. This extension supports whatever Gradle supports and is language/project agnostic, but it can work nicely alongside other extensions like the [Java language support extension](https://github.com/redhat-developer/vscode-java). -- 👉 [All Features](./FEATURES.md) -- 👉 [Architecture Overview](./ARCHITECTURE.md) +👉 [All Features](./FEATURES.md) ## Requirements - [Java >= 8](https://adoptopenjdk.net/) must be installed -- Local Gradle wrapper executables must exist at the root of the workspace folders (either `gradlew` or `gradlew.bat`, depending on your environment) +- Local Gradle wrapper executables must exist at the root of the workspace folders ## Settings @@ -37,13 +36,31 @@ This extension contributes the following settings: This extension supports the following settings: -- `java.home`: Absolute path to JDK home folder used to launch the gradle daemons. (Contributed by [vscode-java](https://github.com/redhat-developer/vscode-java).) +- `java.home`: Absolute path to JDK home folder used to launch the gradle daemons (Contributed by [vscode-java](https://github.com/redhat-developer/vscode-java)) +- `java.import.gradle.user.home`: Setting for GRADLE_HOME (Contributed by [vscode-java](https://github.com/redhat-developer/vscode-java)) + +## Supported Environment Variables + +Most of the standard Java & Gradle environment variables are supported: + +- `JAVE_HOME` (overridden by `java.home`) +- `GRADLE_USER_HOME` (overridden by `java.import.gradle.user.home`) + +### Setting Project Environment Variables -## Usage +You can use an environment manager like [direnv](https://direnv.net/) to set project specific environment variables, or set the variables in the terminal settings within `.vscode/settings.json`, for example: + +```json +{ + "terminal.integrated.env.osx": { + "GRADLE_USER_HOME": "${workspaceFolder}/.gradle" + } +} +``` -Open a Gradle project to use the extension. The extension first starts a process to discover tasks, and progress for this process is reported in the statusbar. Once tasks are discovered, a "Gradle Tasks" view is displayed in the explorer view, where you can view the Gradle project & task hierarchy and run specific tasks. You can also run Gradle tasks via vscode tasks by executing the "Run Task" command from the Command Palette and choosing a Gradle task. +Note, the VS Code settings take precedence over the environment variables. -### Debugging JavaExec Tasks +## Debugging JavaExec Tasks ![Debug Screencast](images/debug-screencast.gif) @@ -63,7 +80,7 @@ To enable this feature you need to specify which tasks can be debugged within yo You should now see a `debug` command next to the `run` command in the Gradle Tasks view. The `debug` command will start the Gradle task with [jdwp](https://docs.oracle.com/en/java/javase/11/docs/specs/jpda/conninv.html#oracle-vm-invocation-options) `jvmArgs` and start the vscode Java debugger. -#### Debugging Limitations +### Debugging Limitations You'll need to remove any `jdwp` options that might have been set in your task configuration (eg via `jvmArgs`). @@ -160,10 +177,6 @@ The reason for the incompatibility is due to the extensions providing the same t -## Contributing - -Refer to [CONTRIBUTING.md](./CONTRIBUTING.md) for instructions on how to run the project. - ## Support For general support queries, use the [#gradle-tasks](https://vscode-dev-community.slack.com/archives/C011NUFTHLM) channel in the [slack development community workspace](https://aka.ms/vscode-dev-community), or @@ -171,10 +184,17 @@ For general support queries, use the [#gradle-tasks](https://vscode-dev-communit - 👉 [Submit a bug report](https://github.com/badsyntax/vscode-gradle/issues/new?assignees=badsyntax&labels=bug&template=bug_report.md&title=) - 👉 [Submit a feature request](https://github.com/badsyntax/vscode-gradle/issues/new?assignees=badsyntax&labels=enhancement&template=feature_request.md&title=) +## Contributing + +Refer to [CONTRIBUTING.md](./CONTRIBUTING.md) for instructions on how to run the project. + +- 👉 [Architecture Overview](./ARCHITECTURE.md) + ## Credits - Originally forked from [Cazzar/vscode-gradle](https://github.com/Cazzar/vscode-gradle) - Inspired by the built-in [npm extension](https://github.com/microsoft/vscode/tree/master/extensions/npm) +- Thanks to all who have submitted bug reports and feedback 👍 ## Release Notes diff --git a/extension/package.json b/extension/package.json index 94b3e677b..07b383e51 100644 --- a/extension/package.json +++ b/extension/package.json @@ -349,21 +349,25 @@ "gradle.enableTasksExplorer": { "type": "boolean", "default": true, + "scope": "resource", "description": "%extension.config.enableTasksExplorer.description%" }, "gradle.debug": { "type": "boolean", "default": false, + "scope": "window", "description": "%extension.config.debug.description%" }, "gradle.focusTaskInExplorer": { "type": "boolean", "default": true, + "scope": "resource", "description": "%extension.config.focusTaskInExplorer.description%" }, "gradle.javaDebug": { "type": "object", "description": "%extension.config.javaDebug.description%", + "scope": "resource", "properties": { "tasks": { "type": "array", @@ -393,6 +397,7 @@ "gradle.taskPresentationOptions": { "type": "object", "description": "%extension.config.taskPresentationOptions.description%", + "scope": "resource", "properties": { "reveal": { "type": "string", diff --git a/extension/src/client.ts b/extension/src/client.ts index 319073e4c..c3d902dfc 100644 --- a/extension/src/client.ts +++ b/extension/src/client.ts @@ -17,6 +17,7 @@ import { Cancelled, GradleBuild, CancelRunTasksRequest, + Environment, } from './proto/gradle_tasks_pb'; import { GradleTasksClient as GrpcClient } from './proto/gradle_tasks_grpc_pb'; @@ -94,15 +95,22 @@ export class GradleTasksClient implements vscode.Disposable { } } - public async getBuild(projectFolder: string): Promise { + public async getBuild( + projectFolder: string, + gradleUserHome: string | null + ): Promise { this.statusBarItem.text = localize( + // TODO 'client.refreshingTasks', - '{0} Gradle: Refreshing Tasks', + '{0} Gradle: Building...', '$(sync~spin)' ); this.statusBarItem.show(); const request = new GetBuildRequest(); request.setProjectDir(projectFolder); + if (gradleUserHome) { + request.setGradleUserHome(gradleUserHome); + } const getBuildStream = this.grpcClient!.getBuild(request); try { return await new Promise((resolve, reject) => { @@ -122,6 +130,9 @@ export class GradleTasksClient implements vscode.Disposable { case GetBuildReply.KindCase.GET_BUILD_RESULT: build = getBuildReply.getGetBuildResult()!.getBuild(); break; + case GetBuildReply.KindCase.ENVIRONMENT: + this.handleGetBuildEnvironment(getBuildReply.getEnvironment()!); + break; } }) .on('error', reject) @@ -148,6 +159,7 @@ export class GradleTasksClient implements vscode.Disposable { task: vscode.Task, args: string[] = [], javaDebugPort: number | null, + gradleUserHome: string | null, onOutput: (output: Output) => void ): Promise { this.statusBarItem.show(); @@ -155,10 +167,13 @@ export class GradleTasksClient implements vscode.Disposable { request.setProjectDir(projectFolder); request.setTask(task.definition.script); request.setJavaDebug(task.definition.javaDebug); + request.setArgsList(args); if (javaDebugPort !== null) { request.setJavaDebugPort(javaDebugPort); } - request.setArgsList(args); + if (gradleUserHome) { + request.setGradleUserHome(gradleUserHome); + } const runTaskStream = this.grpcClient!.runTask(request); try { await new Promise((resolve, reject) => { @@ -303,6 +318,15 @@ export class GradleTasksClient implements vscode.Disposable { } } + private handleGetBuildEnvironment(environment: Environment): void { + const javaEnv = environment.getJavaEnvironment()!; + const gradleEnv = environment.getGradleEnvironment()!; + logger.info('Java Home:', javaEnv.getJavaHome()); + logger.info('JVM Args:', javaEnv.getJvmArgsList().join(',')); + logger.info('Gradle User Home:', gradleEnv.getGradleUserHome()); + logger.info('Gradle Version:', gradleEnv.getGradleVersion()); + } + private handleRunTaskCancelled = ( task: vscode.Task, cancelled: Cancelled diff --git a/extension/src/config.ts b/extension/src/config.ts index 590c0987f..164e612b1 100644 --- a/extension/src/config.ts +++ b/extension/src/config.ts @@ -24,6 +24,12 @@ export function getConfigJavaHome(): string | null { .get('home', null); } +export function getConfigJavaImportGradleUserHome(): string | null { + return vscode.workspace + .getConfiguration('java') + .get('import.gradle.user.home', null); +} + export function getConfigIsDebugEnabled(): boolean { return vscode.workspace .getConfiguration('gradle') diff --git a/extension/src/gradleView.ts b/extension/src/gradleView.ts index fc6493eb6..bf6b5ad99 100644 --- a/extension/src/gradleView.ts +++ b/extension/src/gradleView.ts @@ -236,6 +236,7 @@ export class GradleTasksTreeDataProvider 'gradle:explorerCollapsed', collapsed ); + this.buildTreeItems(); this.render(); } diff --git a/extension/src/logger.ts b/extension/src/logger.ts index 756c1c86e..f305a2e28 100644 --- a/extension/src/logger.ts +++ b/extension/src/logger.ts @@ -22,21 +22,21 @@ export class Logger { return `[${type}] ${message}`; } - public info(message: string): void { - this.log(message, 'info'); + public info(...messages: string[]): void { + this.log(messages.join(' '), 'info'); } - public warning(message: string): void { - this.log(message, 'warning'); + public warning(...messages: string[]): void { + this.log(messages.join(' '), 'warning'); } - public error(message: string): void { - this.log(message, 'error'); + public error(...messages: string[]): void { + this.log(messages.join(' '), 'error'); } - public debug(message: string): void { + public debug(...messages: string[]): void { if (getConfigIsDebugEnabled() || isTest()) { - this.log(message, 'debug'); + this.log(messages.join(' '), 'debug'); } } diff --git a/extension/src/tasks.ts b/extension/src/tasks.ts index fb6ff218d..a89e5a428 100644 --- a/extension/src/tasks.ts +++ b/extension/src/tasks.ts @@ -13,6 +13,7 @@ import { ConfigTaskPresentationOptionsPanelKind, ConfigTaskPresentationOptions, getConfigTaskPresentationOptions, + getConfigJavaImportGradleUserHome, } from './config'; import { logger } from './logger'; import { GradleTasksClient } from './client'; @@ -122,15 +123,14 @@ export function getRestartingTask(task: vscode.Task): vscode.Task | void { return restartingTasks.get(task.definition.id); } -async function hasGradleBuildFile( - folder: vscode.WorkspaceFolder -): Promise { - const files = fg.sync('*{.gradle,.gradle.kts}', { +function getGradleBuildFile(folder: vscode.WorkspaceFolder): string { + const files = fg.sync('!(*settings){.gradle,.gradle.kts}', { onlyFiles: true, cwd: folder.uri.fsPath, deep: 1, + absolute: true, }); - return files.length > 0; + return files[0]; } function getTaskPresentationOptions(): vscode.TaskPresentationOptions { @@ -166,11 +166,15 @@ export class GradleTaskProvider implements vscode.TaskProvider { const allTasks: vscode.Task[] = []; const taskPresentationOptions = getTaskPresentationOptions(); for (const workspaceFolder of folders) { - if ( - getConfigIsAutoDetectionEnabled(workspaceFolder) && - hasGradleBuildFile(workspaceFolder) - ) { - const gradleBuild = await this.getGradleBuild(workspaceFolder); + if (getConfigIsAutoDetectionEnabled(workspaceFolder)) { + const buildFile = getGradleBuildFile(workspaceFolder); + if (!buildFile) { + continue; + } + const gradleBuild = await this.getGradleBuild( + workspaceFolder, + vscode.Uri.file(buildFile) + ); const gradleProject = gradleBuild && gradleBuild.getProject(); if (gradleProject) { allTasks.push( @@ -214,9 +218,19 @@ export class GradleTaskProvider implements vscode.TaskProvider { } private async getGradleBuild( - projectFolder: vscode.WorkspaceFolder + projectFolder: vscode.WorkspaceFolder, + buildFile: vscode.Uri ): Promise { - return await this.client?.getBuild(projectFolder.uri.fsPath); + const gradleUserHome = getConfigJavaImportGradleUserHome(); + const build = await this.client?.getBuild( + projectFolder.uri.fsPath, + gradleUserHome + ); + vscode.commands.executeCommand( + 'gradle.updateJavaProjectConfiguration', + buildFile + ); + return build; } private getVSCodeTasksFromGradleProject( @@ -399,11 +413,13 @@ class CustomBuildTaskTerminal implements vscode.Pseudoterminal { try { const javaDebugEnabled = this.task.definition.javaDebug; const javaDebugPort = javaDebugEnabled ? await getPort() : null; + const gradleUserHome = getConfigJavaImportGradleUserHome(); const runTask = this.client.runTask( this.projectFolder, this.task, args, javaDebugPort, + gradleUserHome, (output: Output): void => { this.handleOutput(output.getMessage().trim()); } diff --git a/proto/gradle_tasks.proto b/proto/gradle_tasks.proto index 0421c323d..fec4e179e 100644 --- a/proto/gradle_tasks.proto +++ b/proto/gradle_tasks.proto @@ -20,7 +20,10 @@ message CancelGetBuildsReply { string message = 1; } message GetTasksRequest { string project_dir = 1; } -message GetBuildRequest { string project_dir = 1; } +message GetBuildRequest { + string project_dir = 1; + string gradle_user_home = 2; + } message GetBuildReply { oneof kind { @@ -28,6 +31,7 @@ message GetBuildReply { Progress progress = 2; Output output = 3; Cancelled cancelled = 4; + Environment environment = 5; } } @@ -47,6 +51,7 @@ message RunTaskRequest { repeated string args = 3; bool java_debug = 4; int32 java_debug_port = 5; + string gradle_user_home = 6; } message RunTaskResult { @@ -105,6 +110,21 @@ message Cancelled { message Progress { string message = 1; } +message Environment { + JavaEnvironment java_environment = 1; + GradleEnvironment gradle_environment = 2; +} + +message JavaEnvironment { + string java_home = 1; + repeated string jvm_args = 2; +} + +message GradleEnvironment { + string gradle_user_home = 1; + string gradle_version = 2; +} + message Output { enum OutputType { STDERR = 0; diff --git a/tasks-server/src/main/java/com/github/badsyntax/gradletasks/GradleTasksService.java b/tasks-server/src/main/java/com/github/badsyntax/gradletasks/GradleTasksService.java index b74a33de0..9a1283c87 100644 --- a/tasks-server/src/main/java/com/github/badsyntax/gradletasks/GradleTasksService.java +++ b/tasks-server/src/main/java/com/github/badsyntax/gradletasks/GradleTasksService.java @@ -21,7 +21,7 @@ public void getBuild(final GetBuildRequest req, if (!projectDir.exists()) { throw new GradleTasksException(String.format(PROJECT_DIR_ERROR, req.getProjectDir())); } - GradleTasksUtil.getBuild(projectDir, responseObserver); + GradleTasksUtil.getBuild(projectDir, req, responseObserver); responseObserver.onCompleted(); } catch (GradleTasksException e) { logger.error(e.getMessage()); @@ -39,8 +39,7 @@ public void runTask(final RunTaskRequest req, if (!projectDir.exists()) { throw new GradleTasksException(String.format(PROJECT_DIR_ERROR, req.getProjectDir())); } - GradleTasksUtil.runTask(projectDir, req.getTask(), req.getArgsList(), req.getJavaDebug(), - req.getJavaDebugPort(), responseObserver); + GradleTasksUtil.runTask(projectDir, req, responseObserver); responseObserver.onCompleted(); } catch (GradleTasksException e) { logger.error(e.getMessage()); diff --git a/tasks-server/src/main/java/com/github/badsyntax/gradletasks/GradleTasksUtil.java b/tasks-server/src/main/java/com/github/badsyntax/gradletasks/GradleTasksUtil.java index dd8bece38..59cfb9b15 100644 --- a/tasks-server/src/main/java/com/github/badsyntax/gradletasks/GradleTasksUtil.java +++ b/tasks-server/src/main/java/com/github/badsyntax/gradletasks/GradleTasksUtil.java @@ -2,9 +2,10 @@ import java.io.ByteArrayOutputStream; import java.io.File; +import java.nio.file.Paths; import java.util.HashMap; -import java.util.List; import java.util.Map; +import com.google.common.base.Strings; import org.gradle.tooling.BuildCancelledException; import org.gradle.tooling.BuildException; import org.gradle.tooling.BuildLauncher; @@ -15,27 +16,35 @@ import org.gradle.tooling.ProjectConnection; import org.gradle.tooling.UnsupportedVersionException; import org.gradle.tooling.exceptions.UnsupportedBuildArgumentException; +import org.gradle.tooling.model.build.BuildEnvironment; import io.grpc.stub.StreamObserver; public class GradleTasksUtil { - private static CancellationTokenPool cancellationTokenPool = new CancellationTokenPool(); + private static final CancellationTokenPool cancellationTokenPool = new CancellationTokenPool(); private static final Object lock = new Object(); + private static final String JAVA_TOOL_OPTIONS_ENV = "JAVA_TOOL_OPTIONS"; private GradleTasksUtil() { } - public static void getBuild(final File projectDir, + public static void getBuild(final File projectDir, final GetBuildRequest req, final StreamObserver responseObserver) throws GradleTasksException { CancellationTokenSource cancellationTokenSource = GradleConnector.newCancellationTokenSource(); GradleConnector gradleConnector = GradleConnector.newConnector().forProjectDirectory(projectDir); + if (!Strings.isNullOrEmpty(req.getGradleUserHome())) { + gradleConnector + .useGradleUserHomeDir(buildGradleUserHomeFile(req.getGradleUserHome(), projectDir)); + } String cancellationKey = projectDir.getAbsolutePath(); cancellationTokenPool.put(CancellationTokenPool.TYPE.GET, cancellationKey, cancellationTokenSource); - try (ProjectConnection projectConnection = gradleConnector.connect()) { + try (ProjectConnection connection = gradleConnector.connect()) { + responseObserver.onNext(buildEnvironmentReply(connection)); + ModelBuilder rootProjectBuilder = - projectConnection.model(org.gradle.tooling.model.GradleProject.class); + connection.model(org.gradle.tooling.model.GradleProject.class); rootProjectBuilder.withCancellationToken(cancellationTokenSource.token()) .addProgressListener((ProgressEvent progressEvent) -> { synchronized (lock) { @@ -85,18 +94,21 @@ public final void onOutputChanged(ByteArrayOutputStream outputMessage) { } } - public static void runTask(final File projectDir, final String task, final List args, - final Boolean javaDebug, final int javaDebugPort, + public static void runTask(final File projectDir, final RunTaskRequest req, final StreamObserver responseObserver) throws GradleTasksException { GradleConnector gradleConnector = GradleConnector.newConnector().forProjectDirectory(projectDir); + if (!Strings.isNullOrEmpty(req.getGradleUserHome())) { + gradleConnector + .useGradleUserHomeDir(buildGradleUserHomeFile(req.getGradleUserHome(), projectDir)); + } CancellationTokenSource cancellationTokenSource = GradleConnector.newCancellationTokenSource(); - String cancellationKey = projectDir.getAbsolutePath() + task; + String cancellationKey = projectDir.getAbsolutePath() + req.getTask(); cancellationTokenPool.put(CancellationTokenPool.TYPE.RUN, cancellationKey, cancellationTokenSource); - try (ProjectConnection projectConnection = gradleConnector.connect()) { + try (ProjectConnection connection = gradleConnector.connect()) { BuildLauncher build = - projectConnection.newBuild().withCancellationToken(cancellationTokenSource.token()) + connection.newBuild().withCancellationToken(cancellationTokenSource.token()) .addProgressListener((ProgressEvent progressEvent) -> { synchronized (lock) { Progress progress = @@ -108,7 +120,6 @@ public static void runTask(final File projectDir, final String task, final List< @Override public final void onOutputChanged(ByteArrayOutputStream outputMessage) { synchronized (lock) { - Output output = Output.newBuilder().setOutputType(Output.OutputType.STDOUT) .setMessage(outputMessage.toString()).build(); RunTaskReply reply = RunTaskReply.newBuilder().setOutput(output).build(); @@ -125,20 +136,20 @@ public final void onOutputChanged(ByteArrayOutputStream outputMessage) { responseObserver.onNext(reply); } } - }).setColorOutput(true).withArguments(args).forTasks(task); - if (Boolean.TRUE.equals(javaDebug)) { - build.setEnvironmentVariables(buildJavaEnvVarsWithJwdp(javaDebugPort)); + }).setColorOutput(true).withArguments(req.getArgsList()).forTasks(req.getTask()); + if (Boolean.TRUE.equals(req.getJavaDebug())) { + build.setEnvironmentVariables(buildJavaEnvVarsWithJwdp(req.getJavaDebugPort())); } build.run(); - RunTaskResult result = - RunTaskResult.newBuilder().setMessage("Successfully run task").setTask(task).build(); + RunTaskResult result = RunTaskResult.newBuilder().setMessage("Successfully run task") + .setTask(req.getTask()).build(); RunTaskReply reply = RunTaskReply.newBuilder().setRunTaskResult(result).build(); responseObserver.onNext(reply); } catch (BuildException | UnsupportedVersionException | UnsupportedBuildArgumentException | IllegalStateException e) { throw new GradleTasksException(e.getMessage(), e); } catch (BuildCancelledException e) { - Cancelled cancelled = Cancelled.newBuilder().setMessage(e.getMessage()).setTask(task) + Cancelled cancelled = Cancelled.newBuilder().setMessage(e.getMessage()).setTask(req.getTask()) .setProjectDir(projectDir.getPath()).build(); RunTaskReply reply = RunTaskReply.newBuilder().setCancelled(cancelled).build(); responseObserver.onNext(reply); @@ -205,10 +216,30 @@ private static GradleProject buildProject(org.gradle.tooling.model.GradleProject private static Map buildJavaEnvVarsWithJwdp(int javaDebugPort) { HashMap envVars = new HashMap<>(System.getenv()); - envVars.put("JAVA_TOOL_OPTIONS", - String.format( - "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=localhost:%d", + envVars.put(JAVA_TOOL_OPTIONS_ENV, + String.format("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=localhost:%d", javaDebugPort)); return envVars; } + + private static File buildGradleUserHomeFile(String gradleUserHome, File projectDir) { + String gradleUserHomePath = Paths.get(gradleUserHome).isAbsolute() ? gradleUserHome + : Paths.get(projectDir.getAbsolutePath(), gradleUserHome).toAbsolutePath().toString(); + return new File(gradleUserHomePath); + } + + private static GetBuildReply buildEnvironmentReply(ProjectConnection connection) { + BuildEnvironment environment = connection.model(BuildEnvironment.class).get(); + org.gradle.tooling.model.build.GradleEnvironment gradleEnvironment = environment.getGradle(); + org.gradle.tooling.model.build.JavaEnvironment javaEnvironment = environment.getJava(); + return GetBuildReply.newBuilder() + .setEnvironment(Environment.newBuilder() + .setGradleEnvironment(GradleEnvironment.newBuilder() + .setGradleUserHome(gradleEnvironment.getGradleUserHome().getAbsolutePath()) + .setGradleVersion(gradleEnvironment.getGradleVersion())) + .setJavaEnvironment(JavaEnvironment.newBuilder() + .setJavaHome(javaEnvironment.getJavaHome().getAbsolutePath()) + .addAllJvmArgs(javaEnvironment.getJvmArguments()))) + .build(); + } }