Skip to content

Commit

Permalink
feat: added option for custom environment variables in Node.js-based …
Browse files Browse the repository at this point in the history
…tasks (#228)

Fixes #220
  • Loading branch information
v1nc3n4 authored Jun 11, 2024
1 parent a87a3a4 commit 93c7eb9
Show file tree
Hide file tree
Showing 34 changed files with 491 additions and 145 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<a href="https://github.com/siouan/frontend-gradle-plugin/releases/tag/v8.1.0"><img src="https://img.shields.io/badge/Latest%20release-8.1.0-blue.svg" alt="Latest release 8.1.0"/></a>
<a href="https://opensource.org/licenses/Apache-2.0"><img src="https://img.shields.io/badge/License-Apache%202.0-green.svg" alt="License Apache 2.0"/></a>
<br/>
<a href="https://github.com/siouan/frontend-gradle-plugin/actions/workflows/build.yml"><img src="https://github.com/siouan/frontend-gradle-plugin/actions/workflows/build.yml/badge.svg?branch=7.0-jdk17" alt="Build status"/></a>
<a href="https://github.com/siouan/frontend-gradle-plugin/actions/workflows/build.yml"><img src="https://github.com/siouan/frontend-gradle-plugin/actions/workflows/build.yml/badge.svg?branch=8.1-jdk17" alt="Build status"/></a>
<a href="https://sonarcloud.io/project/overview?id=siouan_frontend-gradle-plugin"><img src="https://sonarcloud.io/api/project_badges/measure?project=siouan_frontend-gradle-plugin&metric=alert_status" alt="Quality gate status"/></a>
<a href="https://sonarcloud.io/summary/overall?id=siouan_frontend-gradle-plugin"><img src="https://sonarcloud.io/api/project_badges/measure?project=siouan_frontend-gradle-plugin&metric=coverage" alt="Code coverage"/></a>
<a href="https://sonarcloud.io/summary/overall?id=siouan_frontend-gradle-plugin"><img src="https://sonarcloud.io/api/project_badges/measure?project=siouan_frontend-gradle-plugin&metric=reliability_rating" alt="Reliability"/></a>
Expand Down Expand Up @@ -60,6 +60,7 @@ With their feedback, plugin improvement is possible. Special thanks to:
@[jorgheymans](https://github.com/jorgheymans),
@[ludik0](https://github.com/ludik0),
@[marcospereira](https://github.com/marcospereira),
@[mgiorgino-iobeya](https://github.com/mgiorgino-iobeya),
@[mhalbritter](https://github.com/mhalbritter),
@[mike-howell](https://github.com/mike-howell),
@[napstr](https://github.com/napstr),
Expand Down
2 changes: 1 addition & 1 deletion plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ tasks.named<Wrapper>("wrapper") {
distributionType = Wrapper.DistributionType.ALL
}

tasks.withType<Test>() {
tasks.withType<Test> {
useJUnitPlatform()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.siouan.frontendgradleplugin.infrastructure.gradle;

import static org.assertj.core.api.Assertions.assertThat;
import static org.siouan.frontendgradleplugin.FrontendGradlePlugin.INSTALL_FRONTEND_TASK_NAME;
import static org.siouan.frontendgradleplugin.FrontendGradlePlugin.INSTALL_NODE_TASK_NAME;
import static org.siouan.frontendgradleplugin.infrastructure.gradle.InstallCorepackTask.LATEST_VERSION_ARGUMENT;
Expand All @@ -14,12 +15,13 @@
import static org.siouan.frontendgradleplugin.test.TaskTypes.buildNpmTaskDefinition;
import static org.siouan.frontendgradleplugin.test.TaskTypes.buildPnpmTaskDefinition;
import static org.siouan.frontendgradleplugin.test.TaskTypes.buildYarnTaskDefinition;
import static org.siouan.frontendgradleplugin.test.TaskTypes.createJavascriptFile;
import static org.siouan.frontendgradleplugin.test.TaskTypes.createJavascriptFileLoggingProcessTitle;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Map;
import java.util.Set;

import org.gradle.testkit.runner.BuildResult;
Expand Down Expand Up @@ -55,19 +57,20 @@ void setUp() {
}

@Test
void should_run_custom_tasks() throws IOException {
void should_run_custom_tasks_and_forward_environment_variables() throws IOException {
final Path packageJsonDirectoryPath = Files.createDirectory(projectDirectoryPath.resolve("frontend"));
Files.copy(getResourcePath("package-npm.json"), packageJsonDirectoryPath.resolve("package.json"));
final Path temporaryScriptPath = createJavascriptFile(temporaryDirectoryPath.resolve("script.js"));
final Path temporaryScriptPath = createJavascriptFileLoggingProcessTitle(
temporaryDirectoryPath.resolve("script.js"));
final FrontendMapBuilder frontendMapBuilder = new FrontendMapBuilder()
.nodeVersion("20.14.0")
.nodeInstallDirectory(projectDirectoryPath.resolve("node-dist"))
.corepackVersion(LATEST_VERSION_ARGUMENT)
.packageJsonDirectory(packageJsonDirectoryPath)
.verboseModeEnabled(false);
final String runCorepackTaskDefinition = buildCorepackTaskDefinition(RUN_COREPACK_TASK_NAME, "-v");
.packageJsonDirectory(packageJsonDirectoryPath);
final String runNodeTaskDefinition = buildNodeTaskDefinition(RUN_NODE_TASK_NAME,
temporaryScriptPath.toString().replace("\\", "\\\\"));
temporaryScriptPath.toString().replace("\\", "\\\\"),
Map.of("NODE_OPTIONS", "--title=\\\"Run custom node task\\\""));
final String runCorepackTaskDefinition = buildCorepackTaskDefinition(RUN_COREPACK_TASK_NAME, "-v");
final String runNpmTaskDefinition = buildNpmTaskDefinition(RUN_NPM_TASK_NAME, INSTALL_FRONTEND_TASK_NAME,
"run another-script");
final String runPnpmTaskDefinition = buildPnpmTaskDefinition(RUN_PNPM_TASK_NAME, INSTALL_FRONTEND_TASK_NAME,
Expand All @@ -81,6 +84,10 @@ void should_run_custom_tasks() throws IOException {
final BuildResult runNodeTaskResult1 = runGradle(projectDirectoryPath, RUN_NODE_TASK_NAME);

assertTaskOutcomes(runNodeTaskResult1, SUCCESS, RUN_NODE_TASK_NAME, SUCCESS);
assertThat(runNodeTaskResult1.getOutput()).containsIgnoringNewLines("""
> Task :customNodeTask
Run custom node task
""");

final BuildResult runNodeTaskResult2 = runGradle(projectDirectoryPath, RUN_NODE_TASK_NAME);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;

import org.siouan.frontendgradleplugin.infrastructure.gradle.RunCorepack;
Expand All @@ -31,6 +32,12 @@ public static String buildNodeTaskDefinition(final String taskName, final String
return buildTaskDefinition(taskName, RunNode.class, Set.of(INSTALL_NODE_TASK_NAME), script);
}

public static String buildNodeTaskDefinition(final String taskName, final String script,
final Map<String, String> environmentVariables) {
return buildTaskDefinition(taskName, RunNode.class, Set.of(INSTALL_NODE_TASK_NAME), script,
environmentVariables);
}

public static String buildNpmTaskDefinition(final String taskName, final String script) {
return buildTaskDefinition(taskName, RunNpm.class, Set.of(), script);
}
Expand Down Expand Up @@ -73,15 +80,20 @@ public static String buildYarnTaskDefinition(final String taskName, final Set<St
return buildTaskDefinition(taskName, RunYarn.class, dependsOnTaskNames, script);
}

public static Path createJavascriptFile(final Path scriptPath) throws IOException {
public static Path createJavascriptFileLoggingProcessTitle(final Path scriptPath) throws IOException {
try (final Writer buildFileWriter = Files.newBufferedWriter(scriptPath)) {
buildFileWriter.append("console.log('Hello!');\n");
buildFileWriter.append("console.log(process.title);\n");
}
return scriptPath;
}

private static String buildTaskDefinition(final String taskName, final Class<?> taskTypeClass,
final Set<String> dependsOnTaskNames, final String script) {
return buildTaskDefinition(taskName, taskTypeClass, dependsOnTaskNames, script, Map.of());
}

private static String buildTaskDefinition(final String taskName, final Class<?> taskTypeClass,
final Set<String> dependsOnTaskNames, final String script, final Map<String, String> environmentVariables) {
final StringBuilder definition = new StringBuilder();
definition.append("tasks.register('");
definition.append(taskName);
Expand All @@ -98,6 +110,14 @@ private static String buildTaskDefinition(final String taskName, final Class<?>
definition.append(script);
definition.append("'\n");
}
if (!environmentVariables.isEmpty()) {
environmentVariables.forEach((variableName, variableValue) -> definition
.append("environmentVariables.put(\"")
.append(variableName)
.append("\", \"")
.append(variableValue)
.append("\")\n"));
}
definition.append("}\n");
return definition.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;

import lombok.Builder;
Expand Down Expand Up @@ -42,6 +43,14 @@ public class ExecutionSettings {
@ToString.Include
private final List<String> arguments;

/**
* Additional environment variables to pass to the process.
*
* @since 8.1.0
*/
@ToString.Include
private final Map<String, String> environmentVariables;

/**
* Builds execution settings.
*
Expand All @@ -51,10 +60,11 @@ public class ExecutionSettings {
* @param arguments List of arguments.
*/
public ExecutionSettings(final Path workingDirectoryPath, final Set<Path> additionalExecutablePaths,
final Path executablePath, final List<String> arguments) {
final Path executablePath, final List<String> arguments, final Map<String, String> environmentVariables) {
this.workingDirectoryPath = workingDirectoryPath;
this.additionalExecutablePaths = Set.copyOf(additionalExecutablePaths);
this.executablePath = executablePath;
this.arguments = List.copyOf(arguments);
this.environmentVariables = Map.copyOf(environmentVariables);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ public ExecutionSettings execute(final ResolveExecutionSettingsCommand command)
args.add(WINDOWS_EXECUTABLE_AUTOEXIT_FLAG);
// The command that must be executed in the terminal must be a single argument on itself (like if it was
// quoted).
args.add(escapeWhitespacesFromCommandLineToken(executablePath) + " " + command.getScript().trim());
args.add(
escapeWhitespacesFromCommandLineToken(executablePath) + " " + command.getScript().trim());
} else {
executable = UNIX_EXECUTABLE_PATH;
args.add(UNIX_EXECUTABLE_AUTOEXIT_FLAG);
Expand All @@ -97,7 +98,8 @@ public ExecutionSettings execute(final ResolveExecutionSettingsCommand command)
}
}

return new ExecutionSettings(command.getPackageJsonDirectoryPath(), executablePaths, executable, args);
return new ExecutionSettings(command.getPackageJsonDirectoryPath(), executablePaths, executable, args,
command.getEnvironmentVariables());
}

private String escapeWhitespacesFromCommandLineToken(Path path) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.siouan.frontendgradleplugin.domain;

import java.nio.file.Path;
import java.util.Map;

import lombok.Builder;
import lombok.EqualsAndHashCode;
Expand Down Expand Up @@ -45,4 +46,12 @@ public class ResolveExecutionSettingsCommand {
*/
@EqualsAndHashCode.Include
private final String script;

/**
* Additional environment variables to pass to the process.
*
* @since 8.1.0
*/
@EqualsAndHashCode.Include
private final Map<String, String> environmentVariables;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.siouan.frontendgradleplugin.infrastructure.gradle;

import java.io.File;
import java.util.Map;

import org.gradle.api.DefaultTask;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
Expand Down Expand Up @@ -71,6 +73,13 @@ public abstract class AbstractRunCommandTask extends DefaultTask {
*/
protected final Property<String> systemOsName;

/**
* Additional environment variables to pass when executing the script.
*
* @since 8.1.0
*/
protected final MapProperty<String, String> environmentVariables;

AbstractRunCommandTask(final ObjectFactory objectFactory, final ExecOperations execOperations) {
this.execOperations = execOperations;
this.beanRegistryBuildService = objectFactory.property(BeanRegistryBuildService.class);
Expand All @@ -81,6 +90,7 @@ public abstract class AbstractRunCommandTask extends DefaultTask {
this.verboseModeEnabled = objectFactory.property(Boolean.class);
this.systemJvmArch = objectFactory.property(String.class);
this.systemOsName = objectFactory.property(String.class);
this.environmentVariables = objectFactory.mapProperty(String.class, String.class);
}

@Internal
Expand Down Expand Up @@ -108,6 +118,12 @@ public Property<String> getSystemOsName() {
return systemOsName;
}

@Internal
@SuppressWarnings("unused")
public MapProperty<String, String> getEnvironmentVariables() {
return environmentVariables;
}

@Input
public Property<File> getPackageJsonDirectory() {
return packageJsonDirectory;
Expand Down Expand Up @@ -156,6 +172,7 @@ public void execute() throws NonRunnableTaskException, BeanRegistryException {
.nodeInstallDirectoryPath(nodeInstallDirectory.map(File::toPath).get())
.script(script.get())
.platform(platform)
.environmentVariables(environmentVariables.getOrElse(Map.of()))
.build());
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package org.siouan.frontendgradleplugin.infrastructure.gradle;

import static java.util.stream.Collectors.joining;

import java.io.File;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import lombok.Builder;
import lombok.Getter;
Expand All @@ -20,6 +24,8 @@
@Getter
public class ExecSpecAction implements Action<ExecSpec> {

public static final String PATH_VARIABLE_NAME = "PATH";

/**
* Execution settings.
*/
Expand All @@ -38,43 +44,48 @@ public class ExecSpecAction implements Action<ExecSpec> {
@Override
public void execute(final ExecSpec execSpec) {
execSpec.setWorkingDir(executionSettings.getWorkingDirectoryPath().toString());
execSpec.setExecutable(executionSettings.getExecutablePath().toString());
execSpec.setArgs(executionSettings.getArguments());

final Map<String, String> userEnvironmentVariables = new HashMap<>(executionSettings.getEnvironmentVariables());
final Map<String, Object> currentEnvironmentVariables = execSpec.getEnvironment();

// Prepend directories containing the Node and Yarn executables to the 'PATH' environment variable.
// npm is in the same directory than Node, do nothing for it.
final Map<String, Object> environment = execSpec.getEnvironment();
final String pathVariable = findPathVariable(environment);
final String executablePaths = executionSettings
.getAdditionalExecutablePaths()
// Put all user-defined environment variables except an eventual PATH environment variable which is processed
// separately.
final Optional<Map.Entry<String, String>> userPathVariable = userEnvironmentVariables
.entrySet()
.stream()
.map(Path::toString)
.collect(joining(File.pathSeparator));
final StringBuilder pathValue = new StringBuilder();
if (!executablePaths.isEmpty()) {
pathValue.append(executablePaths);
pathValue.append(File.pathSeparatorChar);
}
pathValue.append((String) environment.getOrDefault(pathVariable, ""));
.filter(entry -> entry.getKey().equalsIgnoreCase(PATH_VARIABLE_NAME))
.findAny();
userPathVariable.map(Map.Entry::getKey).ifPresent(userEnvironmentVariables::remove);

execSpec.environment(pathVariable, pathValue.toString());
execSpec.setExecutable(executionSettings.getExecutablePath());
execSpec.setArgs(executionSettings.getArguments());
afterConfiguredConsumer.accept(execSpec);
}
if (!userEnvironmentVariables.isEmpty()) {
execSpec.environment(userEnvironmentVariables);
}

/**
* Finds the name of the 'PATH' variable. Depending on the O/S, it may be have a different case than the exact
* 'PATH' name.
*
* @param environment Map of environment variables.
* @return The name of the 'PATH' variable.
*/
private String findPathVariable(final Map<String, Object> environment) {
final String pathVariable;
if (environment.containsKey("Path")) {
pathVariable = "Path";
} else {
pathVariable = "PATH";
// If the user overrides the 'PATH' environment variable, it takes precedence over the current value in the
// environment. Otherwise, the original 'PATH' is appended, if any.
final Optional<Map.Entry<String, Object>> systemPathVariable = currentEnvironmentVariables
.entrySet()
.stream()
.filter(entry -> entry.getKey().equalsIgnoreCase(PATH_VARIABLE_NAME))
.findAny();
// Let's prepare the value of the PATH variable by using the eventual override from the user, or the system
// value.
final Optional<String> basePathVariableValue = userPathVariable
.map(Map.Entry::getValue)
.or(() -> systemPathVariable.map(Map.Entry::getValue).map(Objects::toString));
// Prepare directories containing executables to be prepended to the 'PATH' environment variable.
final Stream.Builder<String> pathVariableBuilder = Stream.builder();
final Set<Path> additionalExecutablePaths = executionSettings.getAdditionalExecutablePaths();
if (!additionalExecutablePaths.isEmpty() || basePathVariableValue.isPresent()) {
additionalExecutablePaths.forEach(
additionalExecutablePath -> pathVariableBuilder.accept(additionalExecutablePath.toString()));
basePathVariableValue.ifPresent(pathVariableBuilder);
execSpec.environment(PATH_VARIABLE_NAME,
pathVariableBuilder.build().collect(Collectors.joining(File.pathSeparator)));
}
return pathVariable;

afterConfiguredConsumer.accept(execSpec);
}
}
Loading

0 comments on commit 93c7eb9

Please sign in to comment.