From 1d4dd398a9791988f98cfc73628ea2b156afde76 Mon Sep 17 00:00:00 2001 From: Adam <152864218+adam-enko@users.noreply.github.com> Date: Thu, 3 Jul 2025 12:36:53 +0200 Subject: [PATCH 1/3] initial hacky implementation Everything is duplicated into NpmExecSource --- .../com/github/gradle/node/NodeExtension.kt | 54 ++- .../gradle/node/npm/exec/NpmExecResult.kt | 30 ++ .../gradle/node/npm/exec/NpmExecRunner.kt | 67 ++- .../gradle/node/npm/exec/NpmExecSource.kt | 426 ++++++++++++++++++ .../gradle/node/npm/exec/NpmExecSpec.kt | 45 ++ .../github/gradle/node/npm/proxy/NpmProxy.kt | 252 ++++++----- .../github/gradle/node/npm/task/NpmTask.kt | 53 ++- .../com/github/gradle/node/util/Platform.kt | 7 +- .../com/github/gradle/node/util/Provider.kt | 46 +- .../gradle/node/variant/VariantComputer.kt | 106 ++++- .../node/variant/VariantComputerTest.groovy | 12 +- 11 files changed, 894 insertions(+), 204 deletions(-) create mode 100644 src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecResult.kt create mode 100644 src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt create mode 100644 src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSpec.kt diff --git a/src/main/kotlin/com/github/gradle/node/NodeExtension.kt b/src/main/kotlin/com/github/gradle/node/NodeExtension.kt index 378a832d..e63dafc8 100644 --- a/src/main/kotlin/com/github/gradle/node/NodeExtension.kt +++ b/src/main/kotlin/com/github/gradle/node/NodeExtension.kt @@ -1,13 +1,29 @@ package com.github.gradle.node +import com.github.gradle.node.npm.exec.NpmExecResult +import com.github.gradle.node.npm.exec.NpmExecSource +import com.github.gradle.node.npm.exec.NpmExecSpec import com.github.gradle.node.npm.proxy.ProxySettings import com.github.gradle.node.util.Platform +import com.github.gradle.node.variant.VariantComputer +import com.github.gradle.node.variant.computeNodeExec import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.of import org.gradle.kotlin.dsl.property - -open class NodeExtension(project: Project) { +import javax.inject.Inject + +abstract class NodeExtension +@Inject +internal constructor( + project: Project, + private val providers: ProviderFactory, +) { private val cacheDir = project.layout.projectDirectory.dir(".gradle") /** @@ -18,7 +34,7 @@ open class NodeExtension(project: Project) { /** * The directory where npm is installed (when a specific version is defined) */ - val npmWorkDir = project.objects.directoryProperty().convention(cacheDir.dir("npm")) + val npmWorkDir: DirectoryProperty = project.objects.directoryProperty().convention(cacheDir.dir("npm")) /** * The directory where pnpm is installed (when a pnpm task is used) @@ -53,7 +69,7 @@ open class NodeExtension(project: Project) { * If specified, installs it in the npmWorkDir * If empty, the plugin will use the npm command bundled with Node.js */ - val npmVersion = project.objects.property().convention("") + val npmVersion: Property = project.objects.property().convention("") /** * Version of pnpm to use @@ -111,7 +127,7 @@ open class NodeExtension(project: Project) { * If true, it will download node using above parameters * Note that npm is bundled with Node.js */ - val download = project.objects.property().convention(false) + val download: Property = project.objects.property().convention(false) /** * Whether the plugin automatically should add the proxy configuration to npm and yarn commands @@ -168,7 +184,7 @@ open class NodeExtension(project: Project) { /** * Computed path to nodejs directory */ - val resolvedNodeDir = project.objects.directoryProperty() + val resolvedNodeDir: DirectoryProperty = project.objects.directoryProperty() /** * Operating system and architecture @@ -179,7 +195,7 @@ open class NodeExtension(project: Project) { /** * Operating system and architecture */ - val resolvedPlatform = project.objects.property() + val resolvedPlatform: Property = project.objects.property() init { distBaseUrl.set("https://nodejs.org/dist") @@ -193,6 +209,30 @@ open class NodeExtension(project: Project) { nodeProxySettings.set(if (value) ProxySettings.SMART else ProxySettings.OFF) } + @Suppress("UnstableApiUsage") + fun npmExec( + configuration: NpmExecSpec.() -> Unit + ): Provider { + + val vc = VariantComputer() + val nodeDirProvider = resolvedNodeDir + val npmDirProvider = vc.computeNpmDir(this, nodeDirProvider) + val nodeBinDirProvider = vc.computeNodeBinDir(nodeDirProvider, resolvedPlatform) + val npmBinDirProvider = vc.computeNpmBinDir(npmDirProvider, resolvedPlatform) + val nodeExecProvider = computeNodeExec(this, nodeBinDirProvider) + + vc.computeNpmExec(this, npmBinDirProvider) + + return providers.of(NpmExecSource::class) { + parameters.apply(configuration) +// parameters { +// executable +// ignoreExitValue +// workingDir +// } + } + } + companion object { /** * Extension name in Gradle diff --git a/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecResult.kt b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecResult.kt new file mode 100644 index 00000000..492178ee --- /dev/null +++ b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecResult.kt @@ -0,0 +1,30 @@ +package com.github.gradle.node.npm.exec + +import org.gradle.api.GradleException +import org.gradle.process.ExecResult + +class NpmExecResult internal constructor( + val exitValue: Int, + val failure: GradleException?, + val capturedOutput: String, +) { + + internal fun asExecResult(): ExecResult = object : ExecResult { + + override fun assertNormalExitValue(): ExecResult { + if (failure != null) { + throw failure + } + return this + } + + override fun getExitValue(): Int = exitValue + + override fun rethrowFailure(): ExecResult { + assertNormalExitValue() + return this + } + } + + override fun toString(): String = "NpmExecResult(exitValue=$exitValue, failure=$failure)" +} diff --git a/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecRunner.kt b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecRunner.kt index 167aeeb2..dc4909e7 100644 --- a/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecRunner.kt +++ b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecRunner.kt @@ -24,17 +24,26 @@ abstract class NpmExecRunner { project: ProjectApiHelper, extension: NodeExtension, nodeExecConfiguration: NodeExecConfiguration, - variants: VariantComputer + variants: VariantComputer, ): ExecResult { val npmExecConfiguration = NpmExecConfiguration( - "npm" - ) { variantComputer, nodeExtension, npmBinDir -> variantComputer.computeNpmExec(nodeExtension, npmBinDir) } + command = "npm", + commandExecComputer = { variantComputer, nodeExtension, npmBinDir -> + variantComputer.computeNpmExec( + nodeExtension, + npmBinDir + ) + } + ) return executeCommand( - project, - extension, - NpmProxy.addProxyEnvironmentVariables(extension.nodeProxySettings.get(), nodeExecConfiguration), - npmExecConfiguration, - variants + project = project, + extension = extension, + nodeExecConfiguration = NpmProxy.addProxyEnvironmentVariables( + proxySettings = extension.nodeProxySettings.get(), + nodeExecConfiguration = nodeExecConfiguration + ), + npmExecConfiguration = npmExecConfiguration, + variantComputer = variants ) } @@ -78,9 +87,13 @@ abstract class NpmExecRunner { if (executableAndScript.script != null) listOf(executableAndScript.script) else listOf() val args = argsPrefix.plus(nodeExecConfiguration.command) ExecConfiguration( - executableAndScript.executable, args, additionalBinPath, - nodeExecConfiguration.environment, nodeExecConfiguration.workingDir, - nodeExecConfiguration.ignoreExitValue, nodeExecConfiguration.execOverrides + executable = executableAndScript.executable, + args = args, + additionalBinPaths = additionalBinPath, + environment = nodeExecConfiguration.environment, + workingDir = nodeExecConfiguration.workingDir, + ignoreExitValue = nodeExecConfiguration.ignoreExitValue, + execOverrides = nodeExecConfiguration.execOverrides, ) } } @@ -97,18 +110,34 @@ abstract class NpmExecRunner { val nodeExecProvider = computeNodeExec(nodeExtension, nodeBinDirProvider) val executableProvider = npmExecConfiguration.commandExecComputer(variantComputer, nodeExtension, npmBinDirProvider) - val isWindows = nodeExtension.resolvedPlatform.get().isWindows() val npmScriptFileProvider = - computeNpmScriptFile(nodeDirProvider, npmExecConfiguration.command, isWindows) + computeNpmScriptFile(nodeDirProvider, npmExecConfiguration.command, nodeExtension.resolvedPlatform) + return computeExecutable( + npmExecConfiguration.command, + nodeExtension, + executableProvider, + nodeExecProvider, + npmScriptFileProvider + ) + } + + private fun computeExecutable( + command: String, + nodeExtension: NodeExtension, + executableProvider: Provider, + nodeExecProvider: Provider, + npmScriptFileProvider: Provider, + ): Provider { return zip( - nodeExtension.download, nodeExtension.nodeProjectDir, executableProvider, nodeExecProvider, + nodeExtension.download, + nodeExtension.nodeProjectDir, + executableProvider, + nodeExecProvider, npmScriptFileProvider - ).map { - val (download, nodeProjectDir, executable, nodeExec, - npmScriptFile) = it + ).map { (download, nodeProjectDir, executable, nodeExec, npmScriptFile) -> if (download) { val localCommandScript = nodeProjectDir.dir("node_modules/npm/bin") - .file("${npmExecConfiguration.command}-cli.js").asFile + .file("${command}-cli.js").asFile if (localCommandScript.exists()) { return@map ExecutableAndScript(nodeExec, localCommandScript.absolutePath) } else if (!File(executable).exists()) { @@ -121,7 +150,7 @@ abstract class NpmExecRunner { private data class ExecutableAndScript( val executable: String, - val script: String? = null + val script: String? = null, ) private fun computeAdditionalBinPath( diff --git a/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt new file mode 100644 index 00000000..7e527d01 --- /dev/null +++ b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt @@ -0,0 +1,426 @@ +// TODO remove suppress when updating Gradle to a version where ValueSource is stable +@file:Suppress("UnstableApiUsage") + +package com.github.gradle.node.npm.exec + +import com.github.gradle.node.npm.proxy.NpmProxy +import com.github.gradle.node.util.Platform +import com.github.gradle.node.util.mapIf +import org.gradle.api.Action +import org.gradle.api.GradleException +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters +import org.gradle.process.ExecOperations +import org.gradle.process.ExecSpec +import java.io.ByteArrayOutputStream +import java.io.File +import javax.inject.Inject + +/** + * Runs `npm` using a [ValueSource]. + * + * All options can be configured using [NpmExecSource.Parameters], + * but this is a delicate API and may cause issues. + * Prefer using [NpmExecSpec]. + */ +abstract class NpmExecSource @Inject internal constructor( + private val execOps: ExecOperations, +) : ValueSource { + + abstract class Parameters internal constructor() : NpmExecSpec(), ValueSourceParameters { + // /** +// * The `npm` executable. +// * +// * This could either be a path to the `npm` executable file, +// * or the name of the `PATH` executable. +// */ +// abstract val executable: Property + abstract val ignoreExitValue: Property + abstract val workingDir: DirectoryProperty + abstract val npmCommand: ListProperty +// abstract val args: ListProperty + } + + + private val npmCommand get() = parameters.npmCommand.get() + private val nodeProxySettings get() = parameters.nodeProxySettings.get() + private val npmVersion get() = parameters.npmVersion.get() + private val npmWorkDir get() = parameters.npmWorkDir.get() + + private val download: Boolean get() = parameters.download.getOrElse(true) + private val platform get() = parameters.resolvedPlatform.get() + private val nodeProjectDir get() = parameters.nodeProjectDir.get() + + private val resolvedPlatform get() = parameters.resolvedPlatform.get() + private val resolvedNodeDir get() = parameters.resolvedNodeDir.get() + + + override fun obtain(): NpmExecResult { + val command = parameters.npmCommand.get().plus(parameters.arguments.get()) + val nodeExecConfiguration = + NodeExecConfiguration( + command = command, + environment = parameters.environment.orNull.orEmpty(), + workingDir = parameters.workingDir.asFile.orNull, +// ignoreExitValue = ignoreExitValue.get(), +// execOverrides = execOverrides.orNull, + ) + return executeNpmCommand(nodeExecConfiguration) + } + + private fun computeEnvironment( + execConfiguration: ExecConfiguration + ): Map { + return mutableMapOf().apply { + if (parameters.includeSystemEnvironment.orNull == true) { + putAll(System.getenv()) + } + putAll(parameters.environment.get()) + val additionalBinPaths = execConfiguration.additionalBinPaths + if (additionalBinPaths.isNotEmpty()) { + // Take care of Windows environments that may contain "Path" OR "PATH" - both existing + // possibly (but not in parallel as of now) + val pathEnvironmentVariableName = if (get("Path") != null) "Path" else "PATH" + val originalPath = get(pathEnvironmentVariableName) + val newPath = (additionalBinPaths + originalPath).filterNotNull().joinToString(File.pathSeparator) + put(pathEnvironmentVariableName, newPath) + } + } + } + + + private fun executeNpmCommand( +// project: ProjectApiHelper, +// extension: NodeExtension, + nodeExecConfiguration: NodeExecConfiguration, +// variants: VariantComputer, + ): NpmExecResult { + val npmExecConfiguration = NpmExecConfiguration( + command = "npm", + commandExecComputer = { npmBinDir -> + computeNpmExec( + npmBinDir + ) + } + ) + + val proxiedEnvVars = NpmProxy.createProxyEnvironmentVariables( + proxySettings = nodeProxySettings, + nodeExecConfigurationEnvironment = nodeExecConfiguration.environment + ) + + val proxiedNodeExecConfiguration = nodeExecConfiguration.copy(environment = proxiedEnvVars) + + return executeCommand( +// project = project, +// extension = extension, + nodeExecConfiguration = proxiedNodeExecConfiguration, + npmExecConfiguration = npmExecConfiguration, +// variantComputer = variants + ) + } + +//private fun executeNpxCommand( +// project: ProjectApiHelper, +// extension: NodeExtension, +// nodeExecConfiguration: NodeExecConfiguration, +// variants: VariantComputer +// ): ExecResult { +// val npxExecConfiguration = NpmExecConfiguration("npx") { variantComputer, parameters, npmBinDir -> +// variantComputer.computeNpxExec(parameters, npmBinDir) +// } +// +// return executeCommand(project, extension, nodeExecConfiguration, npxExecConfiguration, variants) +// } + + private fun executeCommand( +// project: ProjectApiHelper? = null, +// extension: NodeExtension, + nodeExecConfiguration: NodeExecConfiguration, + npmExecConfiguration: NpmExecConfiguration, +// variants: VariantComputer, + ): NpmExecResult { + val execConfiguration = + computeExecConfiguration(npmExecConfiguration, nodeExecConfiguration) +// val execRunner = ExecRunner() +// return execRunner.execute(project, extension, execConfiguration) + + ByteArrayOutputStream().use { capturedOutput -> + val result = execOps.exec { + + executable = execConfiguration.executable + args = execConfiguration.args + environment = computeEnvironment(execConfiguration) +// isIgnoreExitValue = execConfiguration.ignoreExitValue + workingDir = computeWorkingDir(nodeProjectDir, execConfiguration) + +// executable = parameters.executable.get() +// args = parameters.arguments.orNull.orEmpty() +// environment = computeEnvironment() + isIgnoreExitValue = parameters.ignoreExitValue.getOrElse(false) +// workingDir = parameters.workingDir.get().asFile + standardOutput = capturedOutput + errorOutput = capturedOutput + } + + val failure = try { + result.rethrowFailure() + null + } catch (ex: GradleException) { + ex + } + + return NpmExecResult( + exitValue = result.exitValue, + failure = failure, + capturedOutput = capturedOutput.toString(), + ) + } + } + + private fun computeExecConfiguration( +// extension: NodeExtension, + npmExecConfiguration: NpmExecConfiguration, + nodeExecConfiguration: NodeExecConfiguration, + ): ExecConfiguration { + val additionalBinPath = computeAdditionalBinPath() + val executableAndScript = computeExecutable(npmExecConfiguration) +// return zip(additionalBinPathProvider, executableAndScriptProvider) +// .map { (additionalBinPath, executableAndScript) -> +// } + val argsPrefix = + if (executableAndScript.script != null) listOf(executableAndScript.script) else listOf() + val args = argsPrefix.plus(nodeExecConfiguration.command) + return ExecConfiguration( + executable = executableAndScript.executable, + args = args, + additionalBinPaths = additionalBinPath, + environment = nodeExecConfiguration.environment, + workingDir = nodeExecConfiguration.workingDir, +// ignoreExitValue = nodeExecConfiguration.ignoreExitValue, + execOverrides = nodeExecConfiguration.execOverrides, + ) + } + + private fun computeExecutable( +// parameters: NodeExtension, + npmExecConfiguration: NpmExecConfiguration, +// variantComputer: VariantComputer + ): ExecutableAndScript { + val nodeDirProvider = parameters.resolvedNodeDir.get() + val npmDirProvider = computeNpmDir(nodeDirProvider) + val nodeBinDirProvider = computeNodeBinDir(nodeDirProvider, parameters.resolvedPlatform.get()) + val npmBinDirProvider = computeNpmBinDir(npmDirProvider, parameters.resolvedPlatform.get()) + val nodeExecProvider = computeNodeExec(nodeBinDirProvider) + val executableProvider = + computeNpmExec(npmBinDirProvider) + val npmScriptFileProvider = + computeNpmScriptFile(nodeDirProvider, npmExecConfiguration.command, resolvedPlatform) + return computeExecutable( + npmExecConfiguration.command, + executableProvider, + nodeExecProvider, + npmScriptFileProvider + ) + } + + private fun computeExecutable( + command: String, + executable: String, + nodeExec: String, + npmScriptFile: String, + ): ExecutableAndScript { + if (download) { + val localCommandScript = nodeProjectDir.dir("node_modules/npm/bin") + .file("${command}-cli.js").asFile + if (localCommandScript.exists()) { + return ExecutableAndScript(nodeExec, localCommandScript.absolutePath) + } else if (!File(executable).exists()) { + return ExecutableAndScript(nodeExec, npmScriptFile) + } + } + return ExecutableAndScript(executable) + } + + private data class ExecutableAndScript( + val executable: String, + val script: String? = null, + ) + + private fun computeAdditionalBinPath(): List { + if (!download) { + return emptyList() + } + val nodeDirProvider = resolvedNodeDir + val nodeBinDirProvider = computeNodeBinDir(nodeDirProvider, resolvedPlatform) + val npmDirProvider = computeNpmDir(nodeDirProvider) + val npmBinDirProvider = computeNpmBinDir(npmDirProvider, resolvedPlatform) + return listOf(npmBinDirProvider, nodeBinDirProvider).map { file -> file.asFile.absolutePath } + } + + + /** + * Get the expected node binary directory, taking Windows specifics into account. + */ + private fun computeNodeBinDir( + nodeDirProvider: Directory, + platform: Platform, + ): Directory = + computeProductBinDir(nodeDirProvider, platform) + + /** + * Get the expected node binary name, node.exe on Windows and node everywhere else. + */ + private fun computeNodeExec( + nodeBinDir: Directory + ): String { + return if (download) { + val nodeCommand = if (platform.isWindows()) "node.exe" else "node" + nodeBinDir.dir(nodeCommand).asFile.absolutePath + } else { + "node" + } + } + + /** + * Get the expected directory for a given npm version. + */ + private fun computeNpmDir( + nodeDir: Directory, + ): Directory { + return if (npmVersion.isNotBlank()) { + val directoryName = "npm-v${npmVersion}" + npmWorkDir.dir(directoryName) + } else { + nodeDir + } + } + + /** + * Get the expected npm binary directory, taking Windows specifics into account. + */ + private fun computeNpmBinDir( + npmDirProvider: Directory, + platform: Platform + ): Directory = + computeProductBinDir(npmDirProvider, platform) + + /** + * Get the expected node binary name, npm.cmd on Windows and npm everywhere else. + * + * Can be overridden by setting npmCommand. + */ + private fun computeNpmExec(npmBinDirProvider: Directory): String { + return computeExec( +// nodeExtension = nodeExtension, + binDirProvider = npmBinDirProvider, + configurationCommand = npmCommand.single(), + ) + } + +// /** +// * Get the expected node binary name, npx.cmd on Windows and npx everywhere else. +// * +// * Can be overridden by setting npxCommand. +// */ +//private fun computeNpxExec(nodeExtension: NodeExtension, npmBinDirProvider: Directory): Provider { +// return computeExec( +// nodeExtension, +// npmBinDirProvider, +// parameters.npxCommand, "npx", "npx.cmd" +// ) +// } + + /** + * Compute the path for a given command, from a given binary directory, taking Windows into account + */ + private fun computeExec( + binDirProvider: Directory, + configurationCommand: String, + unixCommand: String = "npm", + windowsCommand: String = "npm.cmd", + ): String { + val command = if (resolvedPlatform.isWindows()) { + configurationCommand.mapIf({ it == unixCommand }) { windowsCommand } + } else { + configurationCommand + } + return if (download) { + binDirProvider.dir(command).asFile.absolutePath + } else { + command + } + } + + private fun computeProductBinDir( + productDirProvider: Directory, + platform: Platform + ): Directory { + return if (platform.isWindows()) { + productDirProvider.dir("bin") + } else { + productDirProvider + } + } + + + private data class ExecConfiguration( + val executable: String, + val args: List = listOf(), + val additionalBinPaths: List = listOf(), + val environment: Map = mapOf(), + val workingDir: File? = null, +// val ignoreExitValue: Boolean = false, + val execOverrides: Action? = null + ) + +// internal typealias CommandExecComputer = ( +// variantComputer: VariantComputer, +// nodeExtension: NodeExtension, +// npmBinDir: Provider, +// ) -> Provider + + private data class NpmExecConfiguration( + val command: String, + val commandExecComputer: ( +// variantComputer: VariantComputer, +// nodeExtension: NodeExtension, + npmBinDir: Directory, + ) -> String, + ) + + private fun computeWorkingDir( + nodeProjectDir: Directory, + execConfiguration: ExecConfiguration + ): File? { + val workingDir = execConfiguration.workingDir ?: nodeProjectDir.asFile + workingDir.mkdirs() + return workingDir + } + + private fun computeNpmScriptFile( + nodeDir: Directory, + command: String, + platform: Platform, + ): String { + return if (platform.isWindows()) { + nodeDir.dir("node_modules/npm/bin/$command-cli.js").asFile.path + } else { + nodeDir.dir("lib/node_modules/npm/bin/$command-cli.js").asFile.path + } + } + + + private data class NodeExecConfiguration( + val command: List = listOf(), + val environment: Map = mapOf(), + val workingDir: File? = null, +// val ignoreExitValue: Boolean = false, + val execOverrides: Action? = null + ) + +} diff --git a/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSpec.kt b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSpec.kt new file mode 100644 index 00000000..a74afe77 --- /dev/null +++ b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSpec.kt @@ -0,0 +1,45 @@ +package com.github.gradle.node.npm.exec + +import com.github.gradle.node.npm.proxy.ProxySettings +import com.github.gradle.node.util.Platform +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property + +/** + * Configurable options for executing `npm`. + * + * Intended for use in buildscripts by users + */ +abstract class NpmExecSpec internal constructor() { + abstract val arguments: ListProperty + abstract val environment: MapProperty + abstract val includeSystemEnvironment: Property +// abstract val additionalBinPaths: ListProperty + + /** @see com.github.gradle.node.NodeExtension.download */ + abstract val download: Property + + /** @see com.github.gradle.node.NodeExtension.resolvedNodeDir */ + abstract val resolvedNodeDir: DirectoryProperty + + /** @see com.github.gradle.node.NodeExtension.resolvedPlatform */ + abstract val resolvedPlatform: Property + + /** @see com.github.gradle.node.NodeExtension.npmVersion */ + abstract val npmVersion: Property + +// /** @see com.github.gradle.node.NodeExtension.npmCommand */ +// abstract val npmCommand: Property + + /** @see com.github.gradle.node.NodeExtension.npmWorkDir */ + abstract val npmWorkDir: DirectoryProperty + + /** @see com.github.gradle.node.NodeExtension.nodeProjectDir */ + abstract val nodeProjectDir: DirectoryProperty + + + /** @see com.github.gradle.node.NodeExtension.nodeProxySettings */ + abstract val nodeProxySettings: Property +} diff --git a/src/main/kotlin/com/github/gradle/node/npm/proxy/NpmProxy.kt b/src/main/kotlin/com/github/gradle/node/npm/proxy/NpmProxy.kt index fde5182d..6a8f9856 100644 --- a/src/main/kotlin/com/github/gradle/node/npm/proxy/NpmProxy.kt +++ b/src/main/kotlin/com/github/gradle/node/npm/proxy/NpmProxy.kt @@ -2,142 +2,166 @@ package com.github.gradle.node.npm.proxy import com.github.gradle.node.exec.NodeExecConfiguration import java.net.URLEncoder -import java.util.stream.Collectors.toList -import java.util.stream.Stream import kotlin.text.Charsets.UTF_8 -class NpmProxy { +object NpmProxy { - companion object { - // These are the environment variables that HTTPing applications checks, proxy is on and off. - // FTP skipped in hopes of a better future. - private val proxyVariables = listOf( - "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "PROXY" - ) + // companion object { + // These are the environment variables that HTTPing applications checks, proxy is on and off. + // FTP skipped in hopes of a better future. + private val proxyVariables = listOf( + "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "PROXY" + ) - // And since npm also takes settings in the form of environment variables with the - // NPM_CONFIG_ format, we should check those. Hopefully nobody does this. - // Windows will let you set environment variables with hyphens in them, but shells - // on Linux will fight you. So you'll have to be pretty sneaky to do this. - // I'm adding both here "just in case". - private val npmProxyVariables = listOf( - "NPM_CONFIG_PROXY", "NPM_CONFIG_HTTPS-PROXY", "NPM_CONFIG_HTTPS_PROXY", "NPM_CONFIG_NOPROXY" - ) + // And since npm also takes settings in the form of environment variables with the + // NPM_CONFIG_ format, we should check those. Hopefully nobody does this. + // Windows will let you set environment variables with hyphens in them, but shells + // on Linux will fight you. So you'll have to be pretty sneaky to do this. + // I'm adding both here "just in case". + private val npmProxyVariables = listOf( + "NPM_CONFIG_PROXY", "NPM_CONFIG_HTTPS-PROXY", "NPM_CONFIG_HTTPS_PROXY", "NPM_CONFIG_NOPROXY" + ) - /** - * Creates a map of environment variables with proxy settings. - * - * Will return an empty map if none are set. - */ - fun computeNpmProxyEnvironmentVariables(): Map { - val proxyEnvironmentVariables = computeProxyUrlEnvironmentVariables() - if (proxyEnvironmentVariables.isNotEmpty()) { - addProxyIgnoredHostsEnvironmentVariable(proxyEnvironmentVariables) - } - return proxyEnvironmentVariables.toMap() + /** + * Creates a map of environment variables with proxy settings. + * + * Will return an empty map if none are set. + */ + fun computeNpmProxyEnvironmentVariables(): Map { + val proxyEnvironmentVariables = computeProxyUrlEnvironmentVariables() + if (proxyEnvironmentVariables.isNotEmpty()) { + addProxyIgnoredHostsEnvironmentVariable(proxyEnvironmentVariables) } + return proxyEnvironmentVariables.toMap() + } - /** - * Helper function for deciding whether proxy settings need to be set or not. - */ - fun shouldConfigureProxy(env: Map, settings: ProxySettings): Boolean { - if (settings == ProxySettings.FORCED) { - return true - } else if (settings == ProxySettings.SMART) { - return !hasProxyConfiguration(env) - } - - return false + /** + * Helper function for deciding whether proxy settings need to be set or not. + */ + fun shouldConfigureProxy(env: Map, settings: ProxySettings): Boolean { + if (settings == ProxySettings.FORCED) { + return true + } else if (settings == ProxySettings.SMART) { + return !hasProxyConfiguration(env) } - /** - * Returns true if the given map of environment variables has any - * proxy settings configured. - * - * @param env map of environment variables - */ - fun hasProxyConfiguration(env: Map): Boolean { - return env.keys.any { - proxyVariables.contains(it.toUpperCase()) || npmProxyVariables.contains(it.toUpperCase()) - } - } + return false + } - /** - * Get a list of all known keys that affect the proxy configuration - */ - fun getKnownProxyConfigurationKeys(): Set { - return proxyVariables.plus(npmProxyVariables).toSet() + /** + * Returns true if the given map of environment variables has any + * proxy settings configured. + * + * @param env map of environment variables + */ + fun hasProxyConfiguration(env: Map): Boolean { + return env.keys.any { + proxyVariables.contains(it.toUpperCase()) || npmProxyVariables.contains(it.toUpperCase()) } + } - /** - * Creates a new NodeExecConfiguration with the proxy environment variables configured - */ - fun addProxyEnvironmentVariables(proxySettings: ProxySettings, nodeExecConfiguration: NodeExecConfiguration, - environment: Map = System.getenv()): NodeExecConfiguration { - if (shouldConfigureProxy(environment, proxySettings)) { - val npmProxyEnvironmentVariables = computeNpmProxyEnvironmentVariables() - val environmentVariablesToUnset = if (proxySettings == ProxySettings.FORCED) getKnownProxyConfigurationKeys() - else emptySet() - if (npmProxyEnvironmentVariables.isNotEmpty()) { - val environmentVariables = - nodeExecConfiguration.environment - .minus(environmentVariablesToUnset) - .plus(npmProxyEnvironmentVariables) - return nodeExecConfiguration.copy(environment = environmentVariables) - } + /** + * Get a list of all known keys that affect the proxy configuration + */ + fun getKnownProxyConfigurationKeys(): Set { + return proxyVariables.plus(npmProxyVariables).toSet() + } + + /** + * Creates a new NodeExecConfiguration with the proxy environment variables configured + */ + fun addProxyEnvironmentVariables( + proxySettings: ProxySettings, + nodeExecConfiguration: NodeExecConfiguration, + environment: Map = System.getenv() + ): NodeExecConfiguration { + val environmentVariables = createProxyEnvironmentVariables( + proxySettings, + nodeExecConfiguration.environment, + environment, + ) + return nodeExecConfiguration.copy(environment = environmentVariables) + } + + fun createProxyEnvironmentVariables( + proxySettings: ProxySettings, + nodeExecConfigurationEnvironment: Map, + environment: Map = System.getenv() + ): Map { + if (shouldConfigureProxy(environment, proxySettings)) { + val npmProxyEnvironmentVariables = computeNpmProxyEnvironmentVariables() + val environmentVariablesToUnset = + if (proxySettings == ProxySettings.FORCED) getKnownProxyConfigurationKeys() + else emptySet() + if (npmProxyEnvironmentVariables.isNotEmpty()) { + val environmentVariables = + nodeExecConfigurationEnvironment + .minus(environmentVariablesToUnset) + .plus(npmProxyEnvironmentVariables) + return environmentVariables } - return nodeExecConfiguration } + return emptyMap() + } - private fun computeProxyUrlEnvironmentVariables(): MutableMap { - val proxyArgs = mutableMapOf() - for ((proxyProto, proxyParam) in - listOf(arrayOf("http", "HTTP_PROXY"), arrayOf("https", "HTTPS_PROXY"))) { - var proxyHost = System.getProperty("$proxyProto.proxyHost") - val proxyPort = System.getProperty("$proxyProto.proxyPort") - if (proxyHost != null && proxyPort != null) { - proxyHost = proxyHost.replace("^https?://".toRegex(), "") - val proxyUser = System.getProperty("$proxyProto.proxyUser") - val proxyPassword = System.getProperty("$proxyProto.proxyPassword") - if (proxyUser != null && proxyPassword != null) { - proxyArgs[proxyParam] = - "http://${encode(proxyUser)}:${encode(proxyPassword)}@$proxyHost:$proxyPort" - } else { - proxyArgs[proxyParam] = "http://$proxyHost:$proxyPort" - } + private fun computeProxyUrlEnvironmentVariables(): MutableMap { + val proxyArgs = mutableMapOf() + for ((proxyProto, proxyParam) in + listOf(arrayOf("http", "HTTP_PROXY"), arrayOf("https", "HTTPS_PROXY"))) { + var proxyHost = System.getProperty("$proxyProto.proxyHost") + val proxyPort = System.getProperty("$proxyProto.proxyPort") + if (proxyHost != null && proxyPort != null) { + proxyHost = proxyHost.replace("^https?://".toRegex(), "") + val proxyUser = System.getProperty("$proxyProto.proxyUser") + val proxyPassword = System.getProperty("$proxyProto.proxyPassword") + if (proxyUser != null && proxyPassword != null) { + proxyArgs[proxyParam] = + "http://${encode(proxyUser)}:${encode(proxyPassword)}@$proxyHost:$proxyPort" + } else { + proxyArgs[proxyParam] = "http://$proxyHost:$proxyPort" } } - return proxyArgs } + return proxyArgs + } - private fun encode(value: String): String { - return URLEncoder.encode(value, UTF_8.toString()) - } + private fun encode(value: String): String { + return URLEncoder.encode(value, UTF_8.toString()) + } - private fun addProxyIgnoredHostsEnvironmentVariable(proxyEnvironmentVariables: MutableMap) { - val proxyIgnoredHosts = computeProxyIgnoredHosts() - if (proxyIgnoredHosts.isNotEmpty()) { - proxyEnvironmentVariables["NO_PROXY"] = proxyIgnoredHosts.joinToString(", ") - } + private fun addProxyIgnoredHostsEnvironmentVariable( + proxyEnvironmentVariables: MutableMap, + ) { + val proxyIgnoredHosts = computeProxyIgnoredHosts() + if (proxyIgnoredHosts.isNotEmpty()) { + proxyEnvironmentVariables["NO_PROXY"] = proxyIgnoredHosts.joinToString(", ") } + } - private fun computeProxyIgnoredHosts(): List { - return Stream.of("http.nonProxyHosts", "https.nonProxyHosts") - .map { property -> - val propertyValue = System.getProperty(property) - if (propertyValue != null) { - val hosts = propertyValue.split("|") - return@map hosts - .map { host -> - if (host.contains(":")) host.split(":")[0] - else host - } + private fun computeProxyIgnoredHosts(): List { + return listOf("http.nonProxyHosts", "https.nonProxyHosts") + .map { property -> + val propertyValue = System.getProperty(property) + if (propertyValue != null) { + val hosts = propertyValue.split("|") + return@map hosts + .map { host -> + if (host.contains(":")) host.split(":")[0] + else host } - return@map listOf() - } - .flatMap(List::stream) - .distinct() - .collect(toList()) - } + } + return@map listOf() + } + .flatten() + .distinct() +// .collect(toList()) } + + // } + @Deprecated( + "Replace with regular object", + ReplaceWith("NpmProxy", imports = ["com.github.gradle.node.npm.proxy.NpmProxy"]) + ) + object Companion + } diff --git a/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt b/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt index 4a90c5f3..81455a97 100644 --- a/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt +++ b/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt @@ -2,8 +2,7 @@ package com.github.gradle.node.npm.task import com.github.gradle.node.NodeExtension import com.github.gradle.node.NodePlugin -import com.github.gradle.node.exec.NodeExecConfiguration -import com.github.gradle.node.npm.exec.NpmExecRunner +import com.github.gradle.node.npm.exec.NpmExecSource import com.github.gradle.node.task.BaseTask import com.github.gradle.node.util.DefaultProjectApiHelper import org.gradle.api.Action @@ -13,10 +12,7 @@ import org.gradle.api.tasks.Input import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Optional import org.gradle.api.tasks.TaskAction -import org.gradle.kotlin.dsl.listProperty -import org.gradle.kotlin.dsl.mapProperty -import org.gradle.kotlin.dsl.newInstance -import org.gradle.kotlin.dsl.property +import org.gradle.kotlin.dsl.* import org.gradle.process.ExecSpec import javax.inject.Inject @@ -66,11 +62,44 @@ abstract class NpmTask : BaseTask() { @TaskAction fun exec() { - val command = npmCommand.get().plus(args.get()) - val nodeExecConfiguration = - NodeExecConfiguration(command, environment.get(), workingDir.asFile.orNull, ignoreExitValue.get(), - execOverrides.orNull) - val npmExecRunner = objects.newInstance(NpmExecRunner::class.java) - result = npmExecRunner.executeNpmCommand(projectHelper, nodeExtension, nodeExecConfiguration, variantComputer) + @Suppress("UnstableApiUsage") + val npmExec = providers.of(NpmExecSource::class) { + parameters.arguments.set(args) + parameters.environment.set(environment) +// parameters.includeSystemEnvironment.set(nodeExtension.includeSystemEnvironment) +// parameters.additionalBinPaths.set(nodeExtension.additionalBinPaths) + parameters.download.set(nodeExtension.download) + parameters.resolvedNodeDir.set(nodeExtension.resolvedNodeDir) + parameters.resolvedPlatform.set(nodeExtension.resolvedPlatform) + parameters.npmVersion.set(nodeExtension.npmVersion) + parameters.npmCommand.set(listOf(nodeExtension.npmCommand.get())) + parameters.npmWorkDir.set(nodeExtension.npmWorkDir) + parameters.nodeProjectDir.set(nodeExtension.nodeProjectDir) + parameters.nodeProxySettings.set(nodeExtension.nodeProxySettings) +// parameters.executable.set(nodeExtension.executable) + parameters.ignoreExitValue.set(true) + parameters.workingDir.set(workingDir.asFile.orNull) +// parameters.npmCommand.set(nodeExtension.npmCommand) +// parameters.args.set(args) + } + val result = npmExec.get().asExecResult() + result.rethrowFailure() + this.result = result +// val command = npmCommand.get().plus(args.get()) +// val nodeExecConfiguration = +// NodeExecConfiguration( +// command = command, +// environment = environment.get(), +// workingDir = workingDir.asFile.orNull, +// ignoreExitValue = ignoreExitValue.get(), +// execOverrides = execOverrides.orNull, +// ) +// val npmExecRunner = objects.newInstance(NpmExecRunner::class.java) +// result = npmExecRunner.executeNpmCommand( +// project = projectHelper, +// extension = nodeExtension, +// nodeExecConfiguration = nodeExecConfiguration, +// variants = variantComputer, +// ) } } diff --git a/src/main/kotlin/com/github/gradle/node/util/Platform.kt b/src/main/kotlin/com/github/gradle/node/util/Platform.kt index 381a0831..ac789326 100644 --- a/src/main/kotlin/com/github/gradle/node/util/Platform.kt +++ b/src/main/kotlin/com/github/gradle/node/util/Platform.kt @@ -1,6 +1,11 @@ package com.github.gradle.node.util -data class Platform(val name: String, val arch: String) { +import java.io.Serializable + +data class Platform( + val name: String, + val arch: String, +): Serializable { fun isWindows(): Boolean { return name == "win" } diff --git a/src/main/kotlin/com/github/gradle/node/util/Provider.kt b/src/main/kotlin/com/github/gradle/node/util/Provider.kt index 6097eda8..4964155c 100644 --- a/src/main/kotlin/com/github/gradle/node/util/Provider.kt +++ b/src/main/kotlin/com/github/gradle/node/util/Provider.kt @@ -3,40 +3,46 @@ package com.github.gradle.node.util import org.gradle.api.provider.Provider internal fun zip(aProvider: Provider, bProvider: Provider): Provider> { - return aProvider.flatMap { a -> bProvider.map { b -> Pair(a!!, b!!) } } + return aProvider.flatMap { a -> bProvider.map { b -> Pair(a, b) } } } internal fun zip(aProvider: Provider, bProvider: Provider, cProvider: Provider): Provider> { - return zip(aProvider, bProvider).flatMap { pair -> cProvider.map { c -> Triple(pair.first, pair.second, c!!) } } + return zip(aProvider, bProvider).flatMap { pair -> cProvider.map { c -> Triple(pair.first, pair.second, c) } } } -internal fun zip(aProvider: Provider, bProvider: Provider, cProvider: Provider, - dProvider: Provider): Provider> { +internal fun zip( + aProvider: Provider, bProvider: Provider, cProvider: Provider, + dProvider: Provider +): Provider> { return zip(zip(aProvider, bProvider), zip(cProvider, dProvider)) - .map { pairs -> Tuple4(pairs.first.first, pairs.first.second, pairs.second.first, pairs.second.second) } + .map { pairs -> Tuple4(pairs.first.first, pairs.first.second, pairs.second.first, pairs.second.second) } } internal data class Tuple4( - val first: A, - val second: B, - val third: C, - val fourth: D + val first: A, + val second: B, + val third: C, + val fourth: D ) -internal fun zip(aProvider: Provider, bProvider: Provider, cProvider: Provider, - dProvider: Provider, eProvider: Provider): Provider> { +internal fun zip( + aProvider: Provider, bProvider: Provider, cProvider: Provider, + dProvider: Provider, eProvider: Provider +): Provider> { return zip(zip(aProvider, bProvider), zip(cProvider, dProvider, eProvider)) - .map { pairs -> - Tuple5(pairs.first.first, pairs.first.second, pairs.second.first, pairs.second.second, - pairs.second.third) - } + .map { pairs -> + Tuple5( + pairs.first.first, pairs.first.second, pairs.second.first, pairs.second.second, + pairs.second.third + ) + } } internal data class Tuple5( - val first: A, - val second: B, - val third: C, - val fourth: D, - val fifth: E + val first: A, + val second: B, + val third: C, + val fourth: D, + val fifth: E ) diff --git a/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt b/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt index 0b3d2ff8..ba554e7e 100644 --- a/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt +++ b/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt @@ -12,13 +12,21 @@ import org.gradle.api.provider.Provider /** * Get the expected node binary name, node.exe on Windows and node everywhere else. */ -fun computeNodeExec(nodeExtension: NodeExtension, nodeBinDirProvider: Provider): Provider { - return zip(nodeExtension.download, nodeBinDirProvider).map { - val (download, nodeBinDir) = it +fun computeNodeExec( + nodeExtension: NodeExtension, + nodeBinDirProvider: Provider +): Provider { + return zip( + nodeExtension.download, + nodeBinDirProvider, + nodeExtension.resolvedPlatform, + ).map { (download, nodeBinDir, platform) -> if (download) { - val nodeCommand = if (nodeExtension.resolvedPlatform.get().isWindows()) "node.exe" else "node" + val nodeCommand = if (platform.isWindows()) "node.exe" else "node" nodeBinDir.dir(nodeCommand).asFile.absolutePath - } else "node" + } else { + "node" + } } } @@ -29,6 +37,20 @@ fun computeNpmScriptFile(nodeDirProvider: Provider, command: String, } } +internal fun computeNpmScriptFile( + nodeDirProvider: Provider, + command: String, + platform: Provider, +): Provider { + return zip(nodeDirProvider, platform).map { (nodeDir, platform) -> + if (platform.isWindows()) { + nodeDir.dir("node_modules/npm/bin/$command-cli.js").asFile.path + } else { + nodeDir.dir("lib/node_modules/npm/bin/$command-cli.js").asFile.path + } + } +} + fun computeNodeDir(nodeExtension: NodeExtension): Provider { val osName = nodeExtension.resolvedPlatform.get().name val osArch = nodeExtension.resolvedPlatform.get().arch @@ -50,14 +72,24 @@ internal fun computeExec( binDirProvider: Provider, configurationCommand: Property, unixCommand: String, - windowsCommand: String + windowsCommand: String, ): Provider { - return zip(nodeExtension.download, configurationCommand, binDirProvider).map { - val (download, cfgCommand, binDir) = it - val command = if (nodeExtension.resolvedPlatform.get().isWindows()) { + return zip( + nodeExtension.download, + nodeExtension.resolvedPlatform, + configurationCommand, + binDirProvider, + ).map { (download, resolvedPlatform, cfgCommand, binDir) -> + val command = if (resolvedPlatform.isWindows()) { cfgCommand.mapIf({ it == unixCommand }) { windowsCommand } - } else cfgCommand - if (download) binDir.dir(command).asFile.absolutePath else command + } else { + cfgCommand + } + if (download) { + binDir.dir(command).asFile.absolutePath + } else { + command + } } } @@ -95,7 +127,10 @@ open class VariantComputer { /** * Get the expected node binary directory, taking Windows specifics into account. */ - fun computeNodeBinDir(nodeDirProvider: Provider, platform: Property) = + fun computeNodeBinDir( + nodeDirProvider: Provider, + platform: Property, + ): Provider = computeProductBinDir(nodeDirProvider, platform) /** @@ -113,20 +148,28 @@ open class VariantComputer { /** * Get the expected directory for a given npm version. */ - fun computeNpmDir(nodeExtension: NodeExtension, nodeDirProvider: Provider): Provider { - return zip(nodeExtension.npmVersion, nodeExtension.npmWorkDir, nodeDirProvider).map { - val (npmVersion, npmWorkDir, nodeDir) = it - if (npmVersion.isNotBlank()) { - val directoryName = "npm-v${npmVersion}" - npmWorkDir.dir(directoryName) - } else nodeDir - } + fun computeNpmDir( + nodeExtension: NodeExtension, + nodeDirProvider: Provider, + ): Provider { + return zip(nodeExtension.npmVersion, nodeExtension.npmWorkDir, nodeDirProvider) + .map { (npmVersion, npmWorkDir, nodeDir) -> + if (npmVersion.isNotBlank()) { + val directoryName = "npm-v${npmVersion}" + npmWorkDir.dir(directoryName) + } else { + nodeDir + } + } } /** * Get the expected npm binary directory, taking Windows specifics into account. */ - fun computeNpmBinDir(npmDirProvider: Provider, platform: Property) = + fun computeNpmBinDir( + npmDirProvider: Provider, + platform: Property + ): Provider = computeProductBinDir(npmDirProvider, platform) /** @@ -136,8 +179,11 @@ open class VariantComputer { */ fun computeNpmExec(nodeExtension: NodeExtension, npmBinDirProvider: Provider): Provider { return computeExec( - nodeExtension, npmBinDirProvider, - nodeExtension.npmCommand, "npm", "npm.cmd" + nodeExtension = nodeExtension, + binDirProvider = npmBinDirProvider, + configurationCommand = nodeExtension.npmCommand, + unixCommand = "npm", + windowsCommand = "npm.cmd", ) } @@ -211,8 +257,18 @@ open class VariantComputer { ) } - private fun computeProductBinDir(productDirProvider: Provider, platform: Property) = - if (platform.get().isWindows()) productDirProvider else productDirProvider.map { it.dir("bin") } + private fun computeProductBinDir( + productDirProvider: Provider, + platform: Property + ): Provider { + return platform.flatMap { p -> + if (p.isWindows()) { + productDirProvider.map { it.dir("bin") } + } else { + productDirProvider + } + } + } /** * Get the node archive name in Gradle dependency format, using zip for Windows and tar.gz everywhere else. diff --git a/src/test/groovy/com/github/gradle/node/variant/VariantComputerTest.groovy b/src/test/groovy/com/github/gradle/node/variant/VariantComputerTest.groovy index 2f0d5e93..4111adf9 100644 --- a/src/test/groovy/com/github/gradle/node/variant/VariantComputerTest.groovy +++ b/src/test/groovy/com/github/gradle/node/variant/VariantComputerTest.groovy @@ -23,7 +23,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform("Windows 8", osArch) - def nodeExtension = new NodeExtension(project) + def nodeExtension = project.objects.newInstance(NodeExtension::class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(true) nodeExtension.version.set(version) @@ -68,7 +68,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform(osName, osArch) def project = ProjectBuilder.builder().build() - def nodeExtension = new NodeExtension(project) + def nodeExtension = project.objects.newInstance(NodeExtension::class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(true) nodeExtension.version.set('5.12.0') @@ -113,7 +113,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform(osName, osArch, sysOsArch) def project = ProjectBuilder.builder().build() - def nodeExtension = new NodeExtension(project) + def nodeExtension = project.objects.newInstance(NodeExtension::class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(true) nodeExtension.version.set('5.12.0') @@ -153,7 +153,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform("Windows 8", "x86") def project = ProjectBuilder.builder().build() - def nodeExtension = new NodeExtension(project) + def nodeExtension = project.objects.newInstance(NodeExtension::class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(download) nodeExtension.npmVersion.set(npmVersion) @@ -204,7 +204,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform("Linux", "x86") def project = ProjectBuilder.builder().build() - def nodeExtension = new NodeExtension(project) + def nodeExtension = project.objects.newInstance(NodeExtension::class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(download) nodeExtension.npmVersion.set(npmVersion) @@ -257,7 +257,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform("Linux", "x86") def project = ProjectBuilder.builder().build() - def nodeExtension = new NodeExtension(project) + def nodeExtension = project.objects.newInstance(NodeExtension::class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(download) From f98dcfab72faec3dadd98756284c2bb022caaa65 Mon Sep 17 00:00:00 2001 From: Adam <152864218+adam-enko@users.noreply.github.com> Date: Thu, 3 Jul 2025 15:29:24 +0200 Subject: [PATCH 2/3] tidying, fixing tests... --- .../gradle/node/npm/exec/NpmExecSource.kt | 77 +------------------ .../github/gradle/node/npm/task/NpmTask.kt | 9 ++- .../node/task/NodeTask_integTest.groovy | 56 +++++++------- 3 files changed, 36 insertions(+), 106 deletions(-) diff --git a/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt index 7e527d01..9f8d5c4a 100644 --- a/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt +++ b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt @@ -32,17 +32,9 @@ abstract class NpmExecSource @Inject internal constructor( ) : ValueSource { abstract class Parameters internal constructor() : NpmExecSpec(), ValueSourceParameters { - // /** -// * The `npm` executable. -// * -// * This could either be a path to the `npm` executable file, -// * or the name of the `PATH` executable. -// */ -// abstract val executable: Property abstract val ignoreExitValue: Property abstract val workingDir: DirectoryProperty abstract val npmCommand: ListProperty -// abstract val args: ListProperty } @@ -66,8 +58,6 @@ abstract class NpmExecSource @Inject internal constructor( command = command, environment = parameters.environment.orNull.orEmpty(), workingDir = parameters.workingDir.asFile.orNull, -// ignoreExitValue = ignoreExitValue.get(), -// execOverrides = execOverrides.orNull, ) return executeNpmCommand(nodeExecConfiguration) } @@ -76,7 +66,7 @@ abstract class NpmExecSource @Inject internal constructor( execConfiguration: ExecConfiguration ): Map { return mutableMapOf().apply { - if (parameters.includeSystemEnvironment.orNull == true) { + if (parameters.includeSystemEnvironment.getOrElse(true)) { putAll(System.getenv()) } putAll(parameters.environment.get()) @@ -94,10 +84,7 @@ abstract class NpmExecSource @Inject internal constructor( private fun executeNpmCommand( -// project: ProjectApiHelper, -// extension: NodeExtension, nodeExecConfiguration: NodeExecConfiguration, -// variants: VariantComputer, ): NpmExecResult { val npmExecConfiguration = NpmExecConfiguration( command = "npm", @@ -116,38 +103,17 @@ abstract class NpmExecSource @Inject internal constructor( val proxiedNodeExecConfiguration = nodeExecConfiguration.copy(environment = proxiedEnvVars) return executeCommand( -// project = project, -// extension = extension, nodeExecConfiguration = proxiedNodeExecConfiguration, npmExecConfiguration = npmExecConfiguration, -// variantComputer = variants ) } -//private fun executeNpxCommand( -// project: ProjectApiHelper, -// extension: NodeExtension, -// nodeExecConfiguration: NodeExecConfiguration, -// variants: VariantComputer -// ): ExecResult { -// val npxExecConfiguration = NpmExecConfiguration("npx") { variantComputer, parameters, npmBinDir -> -// variantComputer.computeNpxExec(parameters, npmBinDir) -// } -// -// return executeCommand(project, extension, nodeExecConfiguration, npxExecConfiguration, variants) -// } - private fun executeCommand( -// project: ProjectApiHelper? = null, -// extension: NodeExtension, nodeExecConfiguration: NodeExecConfiguration, npmExecConfiguration: NpmExecConfiguration, -// variants: VariantComputer, ): NpmExecResult { val execConfiguration = computeExecConfiguration(npmExecConfiguration, nodeExecConfiguration) -// val execRunner = ExecRunner() -// return execRunner.execute(project, extension, execConfiguration) ByteArrayOutputStream().use { capturedOutput -> val result = execOps.exec { @@ -155,14 +121,8 @@ abstract class NpmExecSource @Inject internal constructor( executable = execConfiguration.executable args = execConfiguration.args environment = computeEnvironment(execConfiguration) -// isIgnoreExitValue = execConfiguration.ignoreExitValue workingDir = computeWorkingDir(nodeProjectDir, execConfiguration) - -// executable = parameters.executable.get() -// args = parameters.arguments.orNull.orEmpty() -// environment = computeEnvironment() - isIgnoreExitValue = parameters.ignoreExitValue.getOrElse(false) -// workingDir = parameters.workingDir.get().asFile + isIgnoreExitValue = parameters.ignoreExitValue.getOrElse(true) standardOutput = capturedOutput errorOutput = capturedOutput } @@ -183,15 +143,11 @@ abstract class NpmExecSource @Inject internal constructor( } private fun computeExecConfiguration( -// extension: NodeExtension, npmExecConfiguration: NpmExecConfiguration, nodeExecConfiguration: NodeExecConfiguration, ): ExecConfiguration { val additionalBinPath = computeAdditionalBinPath() val executableAndScript = computeExecutable(npmExecConfiguration) -// return zip(additionalBinPathProvider, executableAndScriptProvider) -// .map { (additionalBinPath, executableAndScript) -> -// } val argsPrefix = if (executableAndScript.script != null) listOf(executableAndScript.script) else listOf() val args = argsPrefix.plus(nodeExecConfiguration.command) @@ -201,15 +157,12 @@ abstract class NpmExecSource @Inject internal constructor( additionalBinPaths = additionalBinPath, environment = nodeExecConfiguration.environment, workingDir = nodeExecConfiguration.workingDir, -// ignoreExitValue = nodeExecConfiguration.ignoreExitValue, execOverrides = nodeExecConfiguration.execOverrides, ) } private fun computeExecutable( -// parameters: NodeExtension, npmExecConfiguration: NpmExecConfiguration, -// variantComputer: VariantComputer ): ExecutableAndScript { val nodeDirProvider = parameters.resolvedNodeDir.get() val npmDirProvider = computeNpmDir(nodeDirProvider) @@ -316,25 +269,11 @@ abstract class NpmExecSource @Inject internal constructor( */ private fun computeNpmExec(npmBinDirProvider: Directory): String { return computeExec( -// nodeExtension = nodeExtension, binDirProvider = npmBinDirProvider, configurationCommand = npmCommand.single(), ) } -// /** -// * Get the expected node binary name, npx.cmd on Windows and npx everywhere else. -// * -// * Can be overridden by setting npxCommand. -// */ -//private fun computeNpxExec(nodeExtension: NodeExtension, npmBinDirProvider: Directory): Provider { -// return computeExec( -// nodeExtension, -// npmBinDirProvider, -// parameters.npxCommand, "npx", "npx.cmd" -// ) -// } - /** * Compute the path for a given command, from a given binary directory, taking Windows into account */ @@ -374,21 +313,12 @@ abstract class NpmExecSource @Inject internal constructor( val additionalBinPaths: List = listOf(), val environment: Map = mapOf(), val workingDir: File? = null, -// val ignoreExitValue: Boolean = false, val execOverrides: Action? = null ) -// internal typealias CommandExecComputer = ( -// variantComputer: VariantComputer, -// nodeExtension: NodeExtension, -// npmBinDir: Provider, -// ) -> Provider - private data class NpmExecConfiguration( val command: String, val commandExecComputer: ( -// variantComputer: VariantComputer, -// nodeExtension: NodeExtension, npmBinDir: Directory, ) -> String, ) @@ -414,13 +344,10 @@ abstract class NpmExecSource @Inject internal constructor( } } - private data class NodeExecConfiguration( val command: List = listOf(), val environment: Map = mapOf(), val workingDir: File? = null, -// val ignoreExitValue: Boolean = false, val execOverrides: Action? = null ) - } diff --git a/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt b/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt index 81455a97..9628fc38 100644 --- a/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt +++ b/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt @@ -82,9 +82,12 @@ abstract class NpmTask : BaseTask() { // parameters.npmCommand.set(nodeExtension.npmCommand) // parameters.args.set(args) } - val result = npmExec.get().asExecResult() - result.rethrowFailure() - this.result = result + val result = npmExec.get() + if (result.failure != null) { + logger.error(result.capturedOutput) + throw RuntimeException("$path failed to execute npm command.", result.failure) + } + this.result = result.asExecResult() // val command = npmCommand.get().plus(args.get()) // val nodeExecConfiguration = // NodeExecConfiguration( diff --git a/src/test/groovy/com/github/gradle/node/task/NodeTask_integTest.groovy b/src/test/groovy/com/github/gradle/node/task/NodeTask_integTest.groovy index 9c61b1a4..6cc90ed4 100644 --- a/src/test/groovy/com/github/gradle/node/task/NodeTask_integTest.groovy +++ b/src/test/groovy/com/github/gradle/node/task/NodeTask_integTest.groovy @@ -21,7 +21,7 @@ class NodeTask_integTest extends AbstractIntegTest { copyResources("fixtures/node") when: - def result1 = build("hello") + def result1 = build("hello", "--stacktrace") then: result1.task(":nodeSetup").outcome == TaskOutcome.SUCCESS @@ -29,7 +29,7 @@ class NodeTask_integTest extends AbstractIntegTest { result1.output.contains("Hello World") when: - def result2 = build("hello") + def result2 = build("hello", "--stacktrace") then: result2.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -37,7 +37,7 @@ class NodeTask_integTest extends AbstractIntegTest { !result2.output.contains("Hello World") when: - def result3 = build("hello", "-DchangeScript=true") + def result3 = build("hello", "-DchangeScript=true", "--stacktrace") then: result3.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -45,7 +45,7 @@ class NodeTask_integTest extends AbstractIntegTest { !result3.output.contains("Hello") when: - def result4 = build("hello", "-DchangeScript=true", "-DchangeArgs=true") + def result4 = build("hello", "-DchangeScript=true", "-DchangeArgs=true", "--stacktrace") then: result4.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -54,7 +54,7 @@ class NodeTask_integTest extends AbstractIntegTest { result4.output.contains("Hello Alice") when: - def result5 = build("hello", "-DchangeScript=true", "-DchangeArgs=true") + def result5 = build("hello", "-DchangeScript=true", "-DchangeArgs=true", "--stacktrace") then: result5.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -62,7 +62,7 @@ class NodeTask_integTest extends AbstractIntegTest { when: // Reset build arguments to ensure the next change is not up-to-date - def result6 = build("hello") + def result6 = build("hello", "--stacktrace") then: result6.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -70,7 +70,7 @@ class NodeTask_integTest extends AbstractIntegTest { when: writeFile("simple.js", "console.log('Hello Bobby');") - def result7 = build("hello") + def result7 = build("hello", "--stacktrace") then: result7.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -84,7 +84,7 @@ class NodeTask_integTest extends AbstractIntegTest { result8.task(":executeDirectoryScript").outcome == TaskOutcome.FAILED when: - def result9 = build(":version") + def result9 = build(":version", "--stacktrace") then: result9.task(":version").outcome == TaskOutcome.SUCCESS @@ -101,7 +101,7 @@ class NodeTask_integTest extends AbstractIntegTest { copyResources("fixtures/node-env") when: - def result1 = build("env") + def result1 = build("env", "--stacktrace") then: result1.task(":nodeSetup").outcome == TaskOutcome.SUCCESS @@ -109,14 +109,14 @@ class NodeTask_integTest extends AbstractIntegTest { result1.output.contains("No custom environment") when: - def result2 = build("env") + def result2 = build("env", "--stacktrace") then: result2.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE result2.task(":env").outcome == TaskOutcome.UP_TO_DATE when: - def result3 = build("env", "-DchangeOptions=true") + def result3 = build("env", "-DchangeOptions=true", "--stacktrace") then: result3.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -124,7 +124,7 @@ class NodeTask_integTest extends AbstractIntegTest { result3.output.contains("1000000") when: - def result4 = build("env", "-DchangeOptions=true") + def result4 = build("env", "-DchangeOptions=true", "--stacktrace") then: result4.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -132,14 +132,14 @@ class NodeTask_integTest extends AbstractIntegTest { when: // Reset build arguments to ensure the next change is not up-to-date - def result5 = build("env") + def result5 = build("env", "--stacktrace") then: result5.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE result5.task(":env").outcome == TaskOutcome.SUCCESS when: - def result6 = build("env", "-DchangeEnv=true") + def result6 = build("env", "-DchangeEnv=true", "--stacktrace") then: result6.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -148,7 +148,7 @@ class NodeTask_integTest extends AbstractIntegTest { when: environmentVariables.set("NEW_ENV_VARIABLE", "Let's make the whole environment change") - def result7 = build("env", "-DchangeEnv=true") + def result7 = build("env", "-DchangeEnv=true", "--stacktrace") then: result7.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -156,21 +156,21 @@ class NodeTask_integTest extends AbstractIntegTest { when: // Reset build arguments to ensure the next change is not up-to-date - def result8 = build("env") + def result8 = build("env", "--stacktrace") then: result8.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE result8.task(":env").outcome == TaskOutcome.SUCCESS when: - def result9 = build("env", "-DchangeWorkingDir=true") + def result9 = build("env", "-DchangeWorkingDir=true", "--stacktrace") then: result9.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE result9.task(":env").outcome == TaskOutcome.UP_TO_DATE when: - def result10 = build("env", "-DchangeWorkingDir=true", "--rerun-tasks") + def result10 = build("env", "-DchangeWorkingDir=true", "--rerun-tasks", "--stacktrace") then: result10.task(":nodeSetup").outcome == TaskOutcome.SUCCESS @@ -181,14 +181,14 @@ class NodeTask_integTest extends AbstractIntegTest { when: // Reset build arguments to ensure the next change is not up-to-date - def result11 = build("env") + def result11 = build("env", "--stacktrace") then: result11.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE result11.task(":env").outcome == TaskOutcome.UP_TO_DATE when: - def result12 = build("env", "-DignoreExitValue=true") + def result12 = build("env", "-DignoreExitValue=true", "--stacktrace") then: result12.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -196,7 +196,7 @@ class NodeTask_integTest extends AbstractIntegTest { result12.output.contains("No custom environment") when: - def result13 = build("env", "-Dfail=true", "-DignoreExitValue=true") + def result13 = build("env", "-Dfail=true", "-DignoreExitValue=true", "--stacktrace") then: result13.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -204,7 +204,7 @@ class NodeTask_integTest extends AbstractIntegTest { result13.output.contains("I had to fail") when: - def result14 = build("env", "-Dfail=true", "-DignoreExitValue=true") + def result14 = build("env", "-Dfail=true", "-DignoreExitValue=true", "--stacktrace") then: result14.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -219,7 +219,7 @@ class NodeTask_integTest extends AbstractIntegTest { result15.output.contains("I had to fail") when: - def result16 = build("env", "-DoutputFile=true") + def result16 = build("env", "-DoutputFile=true", "--stacktrace") then: result16.task(":nodeSetup").outcome == TaskOutcome.UP_TO_DATE @@ -230,7 +230,7 @@ class NodeTask_integTest extends AbstractIntegTest { outputFile.text.contains("No custom environment") when: - def result17 = build(":version") + def result17 = build(":version", "--stacktrace") then: result17.task(":version").outcome == TaskOutcome.SUCCESS @@ -264,7 +264,7 @@ class NodeTask_integTest extends AbstractIntegTest { copyResources("fixtures/node-fail-on-project-repos-download") when: - def result = build("hello") + def result = build("hello", "--stacktrace") then: result.task(":nodeSetup").outcome == TaskOutcome.SUCCESS @@ -282,7 +282,7 @@ class NodeTask_integTest extends AbstractIntegTest { copyResources("fixtures/node-fail-on-project-repos-no-download") when: - def result = build("hello") + def result = build("hello", "--stacktrace") then: result.task(":nodeSetup").outcome == TaskOutcome.SKIPPED @@ -300,7 +300,7 @@ class NodeTask_integTest extends AbstractIntegTest { copyResources("fixtures/node-disallow-insecure-protocol") when: - def result = build("hello") + def result = build("hello", "--stacktrace") then: result.task(":nodeSetup").outcome == TaskOutcome.SUCCESS @@ -318,7 +318,7 @@ class NodeTask_integTest extends AbstractIntegTest { copyResources("fixtures/node-allow-insecure-protocol") when: - def result = build("hello") + def result = build("hello", "--stacktrace") then: result.task(":nodeSetup").outcome == TaskOutcome.SUCCESS From 0fcfa30816a462aa44c24b90ebcf77f2f7709c7c Mon Sep 17 00:00:00 2001 From: Adam <152864218+adam-enko@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:23:59 +0200 Subject: [PATCH 3/3] wip fixes... --- .../gradle/node/npm/exec/NpmExecRunner.kt | 23 +- .../gradle/node/npm/exec/NpmExecSource.kt | 12 +- .../github/gradle/node/npm/proxy/NpmProxy.kt | 269 +++++++++--------- .../github/gradle/node/npm/task/NpmTask.kt | 18 +- .../gradle/node/variant/VariantComputer.kt | 14 - .../gradle/node/bun/task/Bun_integTest.groovy | 6 +- .../node/npm/task/NpmInstallTaskTest.groovy | 2 +- .../gradle/node/npm/task/Npm_integTest.groovy | 6 +- .../node/variant/VariantComputerTest.groovy | 12 +- 9 files changed, 163 insertions(+), 199 deletions(-) diff --git a/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecRunner.kt b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecRunner.kt index dc4909e7..e2680e6f 100644 --- a/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecRunner.kt +++ b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecRunner.kt @@ -110,34 +110,19 @@ abstract class NpmExecRunner { val nodeExecProvider = computeNodeExec(nodeExtension, nodeBinDirProvider) val executableProvider = npmExecConfiguration.commandExecComputer(variantComputer, nodeExtension, npmBinDirProvider) + val isWindows = nodeExtension.resolvedPlatform.get().isWindows() val npmScriptFileProvider = - computeNpmScriptFile(nodeDirProvider, npmExecConfiguration.command, nodeExtension.resolvedPlatform) - return computeExecutable( - npmExecConfiguration.command, - nodeExtension, - executableProvider, - nodeExecProvider, - npmScriptFileProvider - ) - } - - private fun computeExecutable( - command: String, - nodeExtension: NodeExtension, - executableProvider: Provider, - nodeExecProvider: Provider, - npmScriptFileProvider: Provider, - ): Provider { + computeNpmScriptFile(nodeDirProvider, npmExecConfiguration.command, isWindows) return zip( nodeExtension.download, nodeExtension.nodeProjectDir, executableProvider, nodeExecProvider, - npmScriptFileProvider + npmScriptFileProvider, ).map { (download, nodeProjectDir, executable, nodeExec, npmScriptFile) -> if (download) { val localCommandScript = nodeProjectDir.dir("node_modules/npm/bin") - .file("${command}-cli.js").asFile + .file("${npmExecConfiguration.command}-cli.js").asFile if (localCommandScript.exists()) { return@map ExecutableAndScript(nodeExec, localCommandScript.absolutePath) } else if (!File(executable).exists()) { diff --git a/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt index 9f8d5c4a..346cd8e5 100644 --- a/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt +++ b/src/main/kotlin/com/github/gradle/node/npm/exec/NpmExecSource.kt @@ -34,11 +34,13 @@ abstract class NpmExecSource @Inject internal constructor( abstract class Parameters internal constructor() : NpmExecSpec(), ValueSourceParameters { abstract val ignoreExitValue: Property abstract val workingDir: DirectoryProperty + abstract val coreNpmCommand: Property abstract val npmCommand: ListProperty } - private val npmCommand get() = parameters.npmCommand.get() + private val coreNpmCommand get() = parameters.coreNpmCommand.get() + private val execNpmCommand get() = parameters.npmCommand.get() private val nodeProxySettings get() = parameters.nodeProxySettings.get() private val npmVersion get() = parameters.npmVersion.get() private val npmWorkDir get() = parameters.npmWorkDir.get() @@ -52,7 +54,7 @@ abstract class NpmExecSource @Inject internal constructor( override fun obtain(): NpmExecResult { - val command = parameters.npmCommand.get().plus(parameters.arguments.get()) + val command = execNpmCommand.plus(parameters.arguments.get()) val nodeExecConfiguration = NodeExecConfiguration( command = command, @@ -263,14 +265,14 @@ abstract class NpmExecSource @Inject internal constructor( computeProductBinDir(npmDirProvider, platform) /** - * Get the expected node binary name, npm.cmd on Windows and npm everywhere else. + * Get the expected node binary name, `npm.cmd` on Windows and `npm` everywhere else. * - * Can be overridden by setting npmCommand. + * Can be overridden by setting `npmCommand`. */ private fun computeNpmExec(npmBinDirProvider: Directory): String { return computeExec( binDirProvider = npmBinDirProvider, - configurationCommand = npmCommand.single(), + configurationCommand = coreNpmCommand, ) } diff --git a/src/main/kotlin/com/github/gradle/node/npm/proxy/NpmProxy.kt b/src/main/kotlin/com/github/gradle/node/npm/proxy/NpmProxy.kt index 6a8f9856..0bc301c8 100644 --- a/src/main/kotlin/com/github/gradle/node/npm/proxy/NpmProxy.kt +++ b/src/main/kotlin/com/github/gradle/node/npm/proxy/NpmProxy.kt @@ -4,164 +4,155 @@ import com.github.gradle.node.exec.NodeExecConfiguration import java.net.URLEncoder import kotlin.text.Charsets.UTF_8 -object NpmProxy { - - // companion object { - // These are the environment variables that HTTPing applications checks, proxy is on and off. - // FTP skipped in hopes of a better future. - private val proxyVariables = listOf( - "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "PROXY" - ) - - // And since npm also takes settings in the form of environment variables with the - // NPM_CONFIG_ format, we should check those. Hopefully nobody does this. - // Windows will let you set environment variables with hyphens in them, but shells - // on Linux will fight you. So you'll have to be pretty sneaky to do this. - // I'm adding both here "just in case". - private val npmProxyVariables = listOf( - "NPM_CONFIG_PROXY", "NPM_CONFIG_HTTPS-PROXY", "NPM_CONFIG_HTTPS_PROXY", "NPM_CONFIG_NOPROXY" - ) - - /** - * Creates a map of environment variables with proxy settings. - * - * Will return an empty map if none are set. - */ - fun computeNpmProxyEnvironmentVariables(): Map { - val proxyEnvironmentVariables = computeProxyUrlEnvironmentVariables() - if (proxyEnvironmentVariables.isNotEmpty()) { - addProxyIgnoredHostsEnvironmentVariable(proxyEnvironmentVariables) - } - return proxyEnvironmentVariables.toMap() - } +class NpmProxy { + + companion object { + // These are the environment variables that HTTPing applications checks, proxy is on and off. + // FTP skipped in hopes of a better future. + private val proxyVariables = listOf( + "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "PROXY" + ) - /** - * Helper function for deciding whether proxy settings need to be set or not. - */ - fun shouldConfigureProxy(env: Map, settings: ProxySettings): Boolean { - if (settings == ProxySettings.FORCED) { - return true - } else if (settings == ProxySettings.SMART) { - return !hasProxyConfiguration(env) + // And since npm also takes settings in the form of environment variables with the + // NPM_CONFIG_ format, we should check those. Hopefully nobody does this. + // Windows will let you set environment variables with hyphens in them, but shells + // on Linux will fight you. So you'll have to be pretty sneaky to do this. + // I'm adding both here "just in case". + private val npmProxyVariables = listOf( + "NPM_CONFIG_PROXY", "NPM_CONFIG_HTTPS-PROXY", "NPM_CONFIG_HTTPS_PROXY", "NPM_CONFIG_NOPROXY" + ) + + /** + * Creates a map of environment variables with proxy settings. + * + * Will return an empty map if none are set. + */ + fun computeNpmProxyEnvironmentVariables(): Map { + val proxyEnvironmentVariables = computeProxyUrlEnvironmentVariables() + if (proxyEnvironmentVariables.isNotEmpty()) { + addProxyIgnoredHostsEnvironmentVariable(proxyEnvironmentVariables) + } + return proxyEnvironmentVariables.toMap() } - return false - } + /** + * Helper function for deciding whether proxy settings need to be set or not. + */ + fun shouldConfigureProxy(env: Map, settings: ProxySettings): Boolean { + if (settings == ProxySettings.FORCED) { + return true + } else if (settings == ProxySettings.SMART) { + return !hasProxyConfiguration(env) + } - /** - * Returns true if the given map of environment variables has any - * proxy settings configured. - * - * @param env map of environment variables - */ - fun hasProxyConfiguration(env: Map): Boolean { - return env.keys.any { - proxyVariables.contains(it.toUpperCase()) || npmProxyVariables.contains(it.toUpperCase()) + return false } - } - /** - * Get a list of all known keys that affect the proxy configuration - */ - fun getKnownProxyConfigurationKeys(): Set { - return proxyVariables.plus(npmProxyVariables).toSet() - } + /** + * Returns true if the given map of environment variables has any + * proxy settings configured. + * + * @param env map of environment variables + */ + fun hasProxyConfiguration(env: Map): Boolean { + return env.keys.any { + proxyVariables.contains(it.toUpperCase()) || npmProxyVariables.contains(it.toUpperCase()) + } + } - /** - * Creates a new NodeExecConfiguration with the proxy environment variables configured - */ - fun addProxyEnvironmentVariables( - proxySettings: ProxySettings, - nodeExecConfiguration: NodeExecConfiguration, - environment: Map = System.getenv() - ): NodeExecConfiguration { - val environmentVariables = createProxyEnvironmentVariables( - proxySettings, - nodeExecConfiguration.environment, - environment, - ) - return nodeExecConfiguration.copy(environment = environmentVariables) - } + /** + * Get a list of all known keys that affect the proxy configuration + */ + fun getKnownProxyConfigurationKeys(): Set { + return proxyVariables.plus(npmProxyVariables).toSet() + } + + /** + * Creates a new NodeExecConfiguration with the proxy environment variables configured + */ + fun addProxyEnvironmentVariables( + proxySettings: ProxySettings, + nodeExecConfiguration: NodeExecConfiguration, + environment: Map = System.getenv() + ): NodeExecConfiguration { + val environmentVariables = createProxyEnvironmentVariables( + proxySettings, + nodeExecConfiguration.environment, + environment, + ) + return nodeExecConfiguration.copy(environment = environmentVariables) + } - fun createProxyEnvironmentVariables( - proxySettings: ProxySettings, - nodeExecConfigurationEnvironment: Map, - environment: Map = System.getenv() - ): Map { - if (shouldConfigureProxy(environment, proxySettings)) { - val npmProxyEnvironmentVariables = computeNpmProxyEnvironmentVariables() - val environmentVariablesToUnset = - if (proxySettings == ProxySettings.FORCED) getKnownProxyConfigurationKeys() - else emptySet() - if (npmProxyEnvironmentVariables.isNotEmpty()) { - val environmentVariables = - nodeExecConfigurationEnvironment - .minus(environmentVariablesToUnset) - .plus(npmProxyEnvironmentVariables) - return environmentVariables + fun createProxyEnvironmentVariables( + proxySettings: ProxySettings, + nodeExecConfigurationEnvironment: Map, + environment: Map = System.getenv() + ): Map { + if (shouldConfigureProxy(environment, proxySettings)) { + val npmProxyEnvironmentVariables = computeNpmProxyEnvironmentVariables() + val environmentVariablesToUnset = + if (proxySettings == ProxySettings.FORCED) getKnownProxyConfigurationKeys() + else emptySet() + if (npmProxyEnvironmentVariables.isNotEmpty()) { + val environmentVariables = + nodeExecConfigurationEnvironment + .minus(environmentVariablesToUnset) + .plus(npmProxyEnvironmentVariables) + return environmentVariables + } } + return emptyMap() } - return emptyMap() - } - private fun computeProxyUrlEnvironmentVariables(): MutableMap { - val proxyArgs = mutableMapOf() - for ((proxyProto, proxyParam) in - listOf(arrayOf("http", "HTTP_PROXY"), arrayOf("https", "HTTPS_PROXY"))) { - var proxyHost = System.getProperty("$proxyProto.proxyHost") - val proxyPort = System.getProperty("$proxyProto.proxyPort") - if (proxyHost != null && proxyPort != null) { - proxyHost = proxyHost.replace("^https?://".toRegex(), "") - val proxyUser = System.getProperty("$proxyProto.proxyUser") - val proxyPassword = System.getProperty("$proxyProto.proxyPassword") - if (proxyUser != null && proxyPassword != null) { - proxyArgs[proxyParam] = - "http://${encode(proxyUser)}:${encode(proxyPassword)}@$proxyHost:$proxyPort" - } else { - proxyArgs[proxyParam] = "http://$proxyHost:$proxyPort" + private fun computeProxyUrlEnvironmentVariables(): MutableMap { + val proxyArgs = mutableMapOf() + for ((proxyProto, proxyParam) in + listOf(arrayOf("http", "HTTP_PROXY"), arrayOf("https", "HTTPS_PROXY"))) { + var proxyHost = System.getProperty("$proxyProto.proxyHost") + val proxyPort = System.getProperty("$proxyProto.proxyPort") + if (proxyHost != null && proxyPort != null) { + proxyHost = proxyHost.replace("^https?://".toRegex(), "") + val proxyUser = System.getProperty("$proxyProto.proxyUser") + val proxyPassword = System.getProperty("$proxyProto.proxyPassword") + if (proxyUser != null && proxyPassword != null) { + proxyArgs[proxyParam] = + "http://${encode(proxyUser)}:${encode(proxyPassword)}@$proxyHost:$proxyPort" + } else { + proxyArgs[proxyParam] = "http://$proxyHost:$proxyPort" + } } } + return proxyArgs } - return proxyArgs - } - private fun encode(value: String): String { - return URLEncoder.encode(value, UTF_8.toString()) - } + private fun encode(value: String): String { + return URLEncoder.encode(value, UTF_8.toString()) + } - private fun addProxyIgnoredHostsEnvironmentVariable( - proxyEnvironmentVariables: MutableMap, - ) { - val proxyIgnoredHosts = computeProxyIgnoredHosts() - if (proxyIgnoredHosts.isNotEmpty()) { - proxyEnvironmentVariables["NO_PROXY"] = proxyIgnoredHosts.joinToString(", ") + private fun addProxyIgnoredHostsEnvironmentVariable( + proxyEnvironmentVariables: MutableMap, + ) { + val proxyIgnoredHosts = computeProxyIgnoredHosts() + if (proxyIgnoredHosts.isNotEmpty()) { + proxyEnvironmentVariables["NO_PROXY"] = proxyIgnoredHosts.joinToString(", ") + } } - } - private fun computeProxyIgnoredHosts(): List { - return listOf("http.nonProxyHosts", "https.nonProxyHosts") - .map { property -> - val propertyValue = System.getProperty(property) - if (propertyValue != null) { - val hosts = propertyValue.split("|") - return@map hosts - .map { host -> - if (host.contains(":")) host.split(":")[0] - else host + private fun computeProxyIgnoredHosts(): List { + return listOf("http.nonProxyHosts", "https.nonProxyHosts") + .map { property -> + val propertyValue = System.getProperty(property) + if (propertyValue != null) { + val hosts = propertyValue.split("|") + return@map hosts.map { host -> + host.substringBefore(":") } + } + return@map listOf() } - return@map listOf() - } - .flatten() - .distinct() -// .collect(toList()) - } - - // } - @Deprecated( - "Replace with regular object", - ReplaceWith("NpmProxy", imports = ["com.github.gradle.node.npm.proxy.NpmProxy"]) - ) - object Companion + .flatten() + .distinct() + } + } } diff --git a/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt b/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt index 9628fc38..0dc95afa 100644 --- a/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt +++ b/src/main/kotlin/com/github/gradle/node/npm/task/NpmTask.kt @@ -66,28 +66,28 @@ abstract class NpmTask : BaseTask() { val npmExec = providers.of(NpmExecSource::class) { parameters.arguments.set(args) parameters.environment.set(environment) -// parameters.includeSystemEnvironment.set(nodeExtension.includeSystemEnvironment) -// parameters.additionalBinPaths.set(nodeExtension.additionalBinPaths) + parameters.npmCommand.set(npmCommand) + + parameters.ignoreExitValue.set(true) + parameters.workingDir.set(workingDir.asFile.orNull) + parameters.download.set(nodeExtension.download) parameters.resolvedNodeDir.set(nodeExtension.resolvedNodeDir) parameters.resolvedPlatform.set(nodeExtension.resolvedPlatform) parameters.npmVersion.set(nodeExtension.npmVersion) - parameters.npmCommand.set(listOf(nodeExtension.npmCommand.get())) parameters.npmWorkDir.set(nodeExtension.npmWorkDir) parameters.nodeProjectDir.set(nodeExtension.nodeProjectDir) parameters.nodeProxySettings.set(nodeExtension.nodeProxySettings) -// parameters.executable.set(nodeExtension.executable) - parameters.ignoreExitValue.set(true) - parameters.workingDir.set(workingDir.asFile.orNull) -// parameters.npmCommand.set(nodeExtension.npmCommand) -// parameters.args.set(args) + parameters.coreNpmCommand.set(nodeExtension.npmCommand) } val result = npmExec.get() if (result.failure != null) { logger.error(result.capturedOutput) throw RuntimeException("$path failed to execute npm command.", result.failure) + } else { + logger.info(result.capturedOutput) } - this.result = result.asExecResult() + this.result = result.asExecResult().rethrowFailure() // val command = npmCommand.get().plus(args.get()) // val nodeExecConfiguration = // NodeExecConfiguration( diff --git a/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt b/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt index ba554e7e..05a6fc13 100644 --- a/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt +++ b/src/main/kotlin/com/github/gradle/node/variant/VariantComputer.kt @@ -37,20 +37,6 @@ fun computeNpmScriptFile(nodeDirProvider: Provider, command: String, } } -internal fun computeNpmScriptFile( - nodeDirProvider: Provider, - command: String, - platform: Provider, -): Provider { - return zip(nodeDirProvider, platform).map { (nodeDir, platform) -> - if (platform.isWindows()) { - nodeDir.dir("node_modules/npm/bin/$command-cli.js").asFile.path - } else { - nodeDir.dir("lib/node_modules/npm/bin/$command-cli.js").asFile.path - } - } -} - fun computeNodeDir(nodeExtension: NodeExtension): Provider { val osName = nodeExtension.resolvedPlatform.get().name val osArch = nodeExtension.resolvedPlatform.get().arch diff --git a/src/test/groovy/com/github/gradle/node/bun/task/Bun_integTest.groovy b/src/test/groovy/com/github/gradle/node/bun/task/Bun_integTest.groovy index f0e7b25e..3e2735b3 100644 --- a/src/test/groovy/com/github/gradle/node/bun/task/Bun_integTest.groovy +++ b/src/test/groovy/com/github/gradle/node/bun/task/Bun_integTest.groovy @@ -14,7 +14,7 @@ class Bun_integTest extends AbstractIntegTest { copyResources("fixtures/javascript-project/", "javascript-project") when: - def result1 = build("build") + def result1 = build("build", "--stacktrace") then: result1.task(":bunInstall").outcome == TaskOutcome.SUCCESS @@ -28,7 +28,7 @@ class Bun_integTest extends AbstractIntegTest { createFile("javascript-project/output-bun/index.js").isFile() when: - def result2 = build("build") + def result2 = build("build", "--stacktrace") then: // Not up-to-date because the bun.lockb now exists @@ -37,7 +37,7 @@ class Bun_integTest extends AbstractIntegTest { result2.task(":buildBun").outcome == TaskOutcome.UP_TO_DATE when: - def result3 = build("build") + def result3 = build("build", "--stacktrace") then: result3.task(":bunInstall").outcome == TaskOutcome.UP_TO_DATE diff --git a/src/test/groovy/com/github/gradle/node/npm/task/NpmInstallTaskTest.groovy b/src/test/groovy/com/github/gradle/node/npm/task/NpmInstallTaskTest.groovy index 5142df68..c90cabf2 100644 --- a/src/test/groovy/com/github/gradle/node/npm/task/NpmInstallTaskTest.groovy +++ b/src/test/groovy/com/github/gradle/node/npm/task/NpmInstallTaskTest.groovy @@ -36,7 +36,7 @@ class NpmInstallTaskTest extends AbstractTaskTest { GradleProxyHelper.setHttpsProxyPort(11235) nodeExtension.nodeProxySettings.set(ProxySettings.OFF) - def task = project.tasks.getByName("npmInstall") + def task = project.tasks.named("npmInstall", NpmInstallTask.class).get() mockProjectApiHelperExec(task) when: diff --git a/src/test/groovy/com/github/gradle/node/npm/task/Npm_integTest.groovy b/src/test/groovy/com/github/gradle/node/npm/task/Npm_integTest.groovy index b549d065..2894d4f4 100644 --- a/src/test/groovy/com/github/gradle/node/npm/task/Npm_integTest.groovy +++ b/src/test/groovy/com/github/gradle/node/npm/task/Npm_integTest.groovy @@ -12,7 +12,7 @@ class Npm_integTest extends AbstractIntegTest { copyResources("fixtures/javascript-project/", "javascript-project") when: - def result1 = build("build") + def result1 = build("build", "--stacktrace") then: result1.task(":npmInstall").outcome == TaskOutcome.SUCCESS @@ -26,7 +26,7 @@ class Npm_integTest extends AbstractIntegTest { createFile("javascript-project/output-npm/index.js").isFile() when: - def result2 = build("build") + def result2 = build("build", "--stacktrace") then: // Not up-to-date because the package-lock.json now exists @@ -35,7 +35,7 @@ class Npm_integTest extends AbstractIntegTest { result2.task(":buildNpm").outcome == TaskOutcome.UP_TO_DATE when: - def result3 = build("build") + def result3 = build("build", "--stacktrace") then: result3.task(":npmInstall").outcome == TaskOutcome.UP_TO_DATE diff --git a/src/test/groovy/com/github/gradle/node/variant/VariantComputerTest.groovy b/src/test/groovy/com/github/gradle/node/variant/VariantComputerTest.groovy index 4111adf9..332f8457 100644 --- a/src/test/groovy/com/github/gradle/node/variant/VariantComputerTest.groovy +++ b/src/test/groovy/com/github/gradle/node/variant/VariantComputerTest.groovy @@ -23,7 +23,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform("Windows 8", osArch) - def nodeExtension = project.objects.newInstance(NodeExtension::class, project) + def nodeExtension = project.objects.newInstance(NodeExtension.class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(true) nodeExtension.version.set(version) @@ -68,7 +68,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform(osName, osArch) def project = ProjectBuilder.builder().build() - def nodeExtension = project.objects.newInstance(NodeExtension::class, project) + def nodeExtension = project.objects.newInstance(NodeExtension.class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(true) nodeExtension.version.set('5.12.0') @@ -113,7 +113,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform(osName, osArch, sysOsArch) def project = ProjectBuilder.builder().build() - def nodeExtension = project.objects.newInstance(NodeExtension::class, project) + def nodeExtension = project.objects.newInstance(NodeExtension.class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(true) nodeExtension.version.set('5.12.0') @@ -153,7 +153,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform("Windows 8", "x86") def project = ProjectBuilder.builder().build() - def nodeExtension = project.objects.newInstance(NodeExtension::class, project) + def nodeExtension = project.objects.newInstance(NodeExtension.class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(download) nodeExtension.npmVersion.set(npmVersion) @@ -204,7 +204,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform("Linux", "x86") def project = ProjectBuilder.builder().build() - def nodeExtension = project.objects.newInstance(NodeExtension::class, project) + def nodeExtension = project.objects.newInstance(NodeExtension.class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(download) nodeExtension.npmVersion.set(npmVersion) @@ -257,7 +257,7 @@ class VariantComputerTest extends Specification { def platform = getPlatform("Linux", "x86") def project = ProjectBuilder.builder().build() - def nodeExtension = project.objects.newInstance(NodeExtension::class, project) + def nodeExtension = project.objects.newInstance(NodeExtension.class, project) nodeExtension.resolvedPlatform.set(platform) nodeExtension.download.set(download)