[Gradle, JS] Add error state to know when operation was failed

This commit is contained in:
Ilya Goncharov
2022-08-08 08:55:47 +00:00
committed by Space
parent 794229d012
commit 28310eb970
8 changed files with 84 additions and 29 deletions
@@ -1564,6 +1564,37 @@ abstract class AbstractKotlin2JsGradlePluginIT(protected val irBackend: Boolean)
} }
} }
} }
@DisplayName("smoothly fail npm install")
@GradleTest
fun testFailNpmInstall(gradleVersion: GradleVersion) {
project("kotlin-js-browser-project", gradleVersion) {
buildGradleKts.modify { originalScript ->
buildString {
append(originalScript)
append(
"""
|
|plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
| val nodejs = the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>()
| tasks.named<org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask>("kotlinNpmInstall") {
| doFirst {
| nodejs.npmResolutionManager.state =
| org.jetbrains.kotlin.gradle.targets.js.npm.KotlinNpmResolutionManager.ResolutionState.Error(GradleException("someSpecialException"))
| }
| }
|}
|
""".trimMargin()
)
}
}
buildAndFail("build") {
assertTasksFailed(":kotlinNpmInstall")
assertOutputContains("someSpecialException")
}
}
}
} }
@JsGradlePluginTests @JsGradlePluginTests
@@ -10,6 +10,7 @@ import org.gradle.api.tasks.*
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency import org.jetbrains.kotlin.gradle.targets.js.RequiredKotlinJsDependency
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.KotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.RequiresNpmDependencies import org.jetbrains.kotlin.gradle.targets.js.npm.RequiresNpmDependencies
import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject import org.jetbrains.kotlin.gradle.targets.js.npm.npmProject
import java.io.File import java.io.File
@@ -53,7 +54,8 @@ abstract class DukatTask(
get() = resolutionManager.requireInstalled( get() = resolutionManager.requireInstalled(
services, services,
logger logger
)[projectPath][compilationName] )?.get(projectPath)?.get(compilationName)
?: throw (resolutionManager.state as KotlinNpmResolutionManager.ResolutionState.Error).wrappedException
@get:Internal @get:Internal
val dts: List<DtsResolver.Dts> val dts: List<DtsResolver.Dts>
@@ -97,7 +99,7 @@ abstract class DukatTask(
@TaskAction @TaskAction
open fun run() { open fun run() {
resolutionManager.checkRequiredDependencies(this, services, logger, projectPath) resolutionManager.checkRequiredDependencies(this)
destinationDir.deleteRecursively() destinationDir.deleteRecursively()
@@ -137,7 +137,7 @@ open class NodeJsRootExtension(@Transient val rootProject: Project) : Configurat
val versions = NpmVersions() val versions = NpmVersions()
internal val npmResolutionManager = KotlinNpmResolutionManager(this) val npmResolutionManager = KotlinNpmResolutionManager(this)
companion object { companion object {
const val EXTENSION_NAME: String = "kotlinNodeJs" const val EXTENSION_NAME: String = "kotlinNodeJs"
@@ -86,7 +86,7 @@ class KotlinNpmResolutionManager(@Transient private val nodeJsSettings: NodeJsRo
(nodeJsSettings ?: unavailableValueError("nodeJsSettings")).rootProject.isInIdeaSync (nodeJsSettings ?: unavailableValueError("nodeJsSettings")).rootProject.isInIdeaSync
} }
internal val resolver = KotlinRootNpmResolver(nodeJsSettings, forceFullResolve) val resolver = KotlinRootNpmResolver(nodeJsSettings, forceFullResolve)
internal abstract class KotlinNpmResolutionManagerStateHolder : BuildService<BuildServiceParameters.None> { internal abstract class KotlinNpmResolutionManagerStateHolder : BuildService<BuildServiceParameters.None> {
@Volatile @Volatile
@@ -101,13 +101,13 @@ class KotlinNpmResolutionManager(@Transient private val nodeJsSettings: NodeJsRo
private val stateHolder get() = stateHolderProvider.get() private val stateHolder get() = stateHolderProvider.get()
private var state: ResolutionState var state: ResolutionState
get() = stateHolder.state ?: ResolutionState.Configuring(resolver) get() = stateHolder.state ?: ResolutionState.Configuring(resolver)
set(value) { set(value) {
stateHolder.state = value stateHolder.state = value
} }
internal sealed class ResolutionState { sealed class ResolutionState {
abstract val npmProjects: List<NpmProject> abstract val npmProjects: List<NpmProject>
class Configuring(val resolver: KotlinRootNpmResolver) : ResolutionState() { class Configuring(val resolver: KotlinRootNpmResolver) : ResolutionState() {
@@ -120,11 +120,16 @@ class KotlinNpmResolutionManager(@Transient private val nodeJsSettings: NodeJsRo
get() = npmProjectsByProjectResolutions(preparedInstallation.projectResolutions) get() = npmProjectsByProjectResolutions(preparedInstallation.projectResolutions)
} }
class Installed(val resolved: KotlinRootNpmResolution) : ResolutionState() { class Installed internal constructor(internal val resolved: KotlinRootNpmResolution) : ResolutionState() {
override val npmProjects: List<NpmProject> override val npmProjects: List<NpmProject>
get() = npmProjectsByProjectResolutions(resolved.projects) get() = npmProjectsByProjectResolutions(resolved.projects)
} }
class Error(val wrappedException: Throwable) : ResolutionState() {
override val npmProjects: List<NpmProject>
get() = emptyList()
}
companion object { companion object {
fun npmProjectsByProjectResolutions( fun npmProjectsByProjectResolutions(
resolutions: Map<String, KotlinProjectNpmResolution> resolutions: Map<String, KotlinProjectNpmResolution>
@@ -156,23 +161,30 @@ class KotlinNpmResolutionManager(@Transient private val nodeJsSettings: NodeJsRo
args: List<String> = emptyList(), args: List<String> = emptyList(),
services: ServiceRegistry, services: ServiceRegistry,
logger: Logger logger: Logger
): KotlinRootNpmResolution { ): KotlinRootNpmResolution? {
synchronized(stateHolder) { synchronized(stateHolder) {
if (state is ResolutionState.Installed) { if (state is ResolutionState.Installed) {
return (state as ResolutionState.Installed).resolved return (state as ResolutionState.Installed).resolved
} }
val installUpToDate = nodeJsSettings?.npmInstallTaskProvider?.get()?.state?.upToDate ?: false if (state is ResolutionState.Error) {
val forceUpToDate = installUpToDate && !forceFullResolve return null
}
val installation = prepareIfNeeded(requireUpToDateReason = reason, logger = logger) return try {
val resolution = installation val installUpToDate = nodeJsSettings?.npmInstallTaskProvider?.get()?.state?.upToDate ?: false
.install(forceUpToDate, args, services, logger) val forceUpToDate = installUpToDate && !forceFullResolve
state = ResolutionState.Installed(resolution)
installation.closePlugins(resolution) val installation = prepareIfNeeded(requireUpToDateReason = reason, logger = logger)
val resolution = installation
return resolution .install(forceUpToDate, args, services, logger)
state = ResolutionState.Installed(resolution)
installation.closePlugins(resolution)
resolution
} catch (e: Exception) {
state = ResolutionState.Error(e)
throw e
}
} }
} }
@@ -215,10 +227,12 @@ class KotlinNpmResolutionManager(@Transient private val nodeJsSettings: NodeJsRo
} }
} }
is ResolutionState.Installed -> error("Project already installed") is ResolutionState.Installed -> error("Project already installed")
is ResolutionState.Error -> throw state1.wrappedException
} }
} }
} }
is ResolutionState.Installed -> error("Project already installed") is ResolutionState.Installed -> error("Project already installed")
is ResolutionState.Error -> throw state0.wrappedException
} }
} }
@@ -230,7 +244,7 @@ class KotlinNpmResolutionManager(@Transient private val nodeJsSettings: NodeJsRo
val resolvedProject = val resolvedProject =
if (forceFullResolve) { if (forceFullResolve) {
installIfNeeded(reason = null, services = services, logger = logger)[projectPath] installIfNeeded(reason = null, services = services, logger = logger)?.get(projectPath) ?: return null
} else { } else {
// may return null only during npm resolution // may return null only during npm resolution
// (it can be called since NpmDependency added to configuration that // (it can be called since NpmDependency added to configuration that
@@ -243,13 +257,16 @@ class KotlinNpmResolutionManager(@Transient private val nodeJsSettings: NodeJsRo
//error("Cannot use NpmDependency before :kotlinNpmInstall task execution") //error("Cannot use NpmDependency before :kotlinNpmInstall task execution")
} }
is ResolutionState.Installed -> state0.resolved[projectPath] is ResolutionState.Installed -> state0.resolved[projectPath]
is ResolutionState.Error -> {
return null
}
} }
} }
return resolvedProject.npmProjectsByNpmDependency[npmDependency] ?: error("NPM project resolved without $this") return resolvedProject.npmProjectsByNpmDependency[npmDependency] ?: error("NPM project resolved without $this")
} }
internal fun <T> checkRequiredDependencies(task: T, services: ServiceRegistry, logger: Logger, projectPath: String) internal fun <T> checkRequiredDependencies(task: T)
where T : RequiresNpmDependencies, where T : RequiresNpmDependencies,
T : Task { T : Task {
val targetRequired = resolver.taskRequirements.byTask[task.path]?.toSet() ?: setOf() val targetRequired = resolver.taskRequirements.byTask[task.path]?.toSet() ?: setOf()
@@ -36,7 +36,7 @@ import org.jetbrains.kotlin.gradle.utils.unavailableValueError
* Regularly tasks share the same instance of this class, but with configuration cache each task that holds a reference to the instance will * Regularly tasks share the same instance of this class, but with configuration cache each task that holds a reference to the instance will
* create an own copy. We use build services as a single storage for the heavy state of this class. * create an own copy. We use build services as a single storage for the heavy state of this class.
*/ */
internal class KotlinRootNpmResolver internal constructor( class KotlinRootNpmResolver internal constructor(
@Transient @Transient
val nodeJs: NodeJsRootExtension?, val nodeJs: NodeJsRootExtension?,
val forceFullResolve: Boolean val forceFullResolve: Boolean
@@ -62,7 +62,7 @@ internal class KotlinRootNpmResolver internal constructor(
@Volatile @Volatile
private var state_: RootResolverState? = RootResolverState.CONFIGURING private var state_: RootResolverState? = RootResolverState.CONFIGURING
private var state var state
get() = state_ ?: resolverStateHolder.get().state get() = state_ ?: resolverStateHolder.get().state
set(value) { set(value) {
if (state_ != null) { if (state_ != null) {
@@ -82,7 +82,7 @@ internal class KotlinRootNpmResolver internal constructor(
it.parameters.rootProjectDir.set(rootProject_.projectDir) it.parameters.rootProjectDir.set(rootProject_.projectDir)
} }
val gradleNodeModules: GradleNodeModulesCache internal val gradleNodeModules: GradleNodeModulesCache
get() = gradleNodeModulesProvider.get().also { get() = gradleNodeModulesProvider.get().also {
it.archiveOperations = archiveOperations it.archiveOperations = archiveOperations
it.fs = fs it.fs = fs
@@ -95,7 +95,7 @@ internal class KotlinRootNpmResolver internal constructor(
it.parameters.rootProjectDir.set(rootProject_.projectDir) it.parameters.rootProjectDir.set(rootProject_.projectDir)
} }
val compositeNodeModules: CompositeNodeModulesCache internal val compositeNodeModules: CompositeNodeModulesCache
get() = compositeNodeModulesProvider.get() get() = compositeNodeModulesProvider.get()
@Transient @Transient
@@ -157,7 +157,7 @@ internal class KotlinRootNpmResolver internal constructor(
return projResolvers return projResolvers
} }
val plugins internal val plugins
get() = plugins_ ?: resolverStateHolder.get().parameters.plugins.get() get() = plugins_ ?: resolverStateHolder.get().parameters.plugins.get()
private val projectResolvers private val projectResolvers
@@ -190,7 +190,7 @@ internal class KotlinRootNpmResolver internal constructor(
} }
} }
operator fun get(projectPath: String) = projectResolvers[projectPath] ?: error("$projectPath is not configured for JS usage") internal operator fun get(projectPath: String) = projectResolvers[projectPath] ?: error("$projectPath is not configured for JS usage")
val compilations: Collection<KotlinJsCompilation> val compilations: Collection<KotlinJsCompilation>
get() = projectResolvers.values.flatMap { it.compilationResolvers.map { it.compilation } } get() = projectResolvers.values.flatMap { it.compilationResolvers.map { it.compilation } }
@@ -198,7 +198,7 @@ internal class KotlinRootNpmResolver internal constructor(
internal fun getPackageJsonHandlers(projectPath: String, compilationDisambiguatedName: String): List<PackageJson.() -> Unit> = internal fun getPackageJsonHandlers(projectPath: String, compilationDisambiguatedName: String): List<PackageJson.() -> Unit> =
resolverStateHolder.get().parameters.packageJsonHandlers.get()["$projectPath:$compilationDisambiguatedName"] ?: emptyList() resolverStateHolder.get().parameters.packageJsonHandlers.get()["$projectPath:$compilationDisambiguatedName"] ?: emptyList()
fun findDependentResolver(src: Project, target: Project): List<KotlinCompilationNpmResolver>? { internal fun findDependentResolver(src: Project, target: Project): List<KotlinCompilationNpmResolver>? {
// todo: proper finding using KotlinTargetComponent.findUsageContext // todo: proper finding using KotlinTargetComponent.findUsageContext
val targetResolver = this[target.path] val targetResolver = this[target.path]
val mainCompilations = targetResolver.compilationResolvers.filter { it.compilation.isMain() } val mainCompilations = targetResolver.compilationResolvers.filter { it.compilation.isMain() }
@@ -224,6 +224,7 @@ internal class KotlinRootNpmResolver internal constructor(
containsIrJs = true containsIrJs = true
} }
} }
else -> { else -> {
check(!containsLegacyJs) { errorMessage } check(!containsLegacyJs) { errorMessage }
containsLegacyJs = true containsLegacyJs = true
@@ -279,6 +280,9 @@ internal class KotlinRootNpmResolver internal constructor(
logger: Logger logger: Logger
): KotlinRootNpmResolution { ): KotlinRootNpmResolution {
synchronized(projectResolvers) { synchronized(projectResolvers) {
if (state == RootResolverState.INSTALLED) {
return KotlinRootNpmResolution(rootProject, projectResolutions)
}
check(state == RootResolverState.PROJECTS_CLOSED) { check(state == RootResolverState.PROJECTS_CLOSED) {
"Projects must be closed" "Projects must be closed"
} }
@@ -9,6 +9,7 @@ import org.gradle.api.DefaultTask
import org.gradle.api.tasks.* import org.gradle.api.tasks.*
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin
import org.jetbrains.kotlin.gradle.targets.js.npm.KotlinNpmResolutionManager
import org.jetbrains.kotlin.gradle.targets.js.npm.asNpmEnvironment import org.jetbrains.kotlin.gradle.targets.js.npm.asNpmEnvironment
import org.jetbrains.kotlin.gradle.utils.unavailableValueError import org.jetbrains.kotlin.gradle.utils.unavailableValueError
import java.io.File import java.io.File
@@ -70,7 +71,7 @@ open class KotlinNpmInstallTask : DefaultTask() {
args = args, args = args,
services = services, services = services,
logger = logger logger = logger
) ) ?: throw (resolutionManager.state as KotlinNpmResolutionManager.ResolutionState.Error).wrappedException
} }
companion object { companion object {
@@ -157,7 +157,7 @@ constructor(
} }
override fun executeTests() { override fun executeTests() {
npmResolutionManager.checkRequiredDependencies(task = this, services = services, logger = logger, projectPath = projectPath) npmResolutionManager.checkRequiredDependencies(task = this)
super.executeTests() super.executeTests()
} }
@@ -297,7 +297,7 @@ constructor(
@TaskAction @TaskAction
fun doExecute() { fun doExecute() {
resolutionManager.checkRequiredDependencies(task = this, services = services, logger = logger, projectPath = projectPath) resolutionManager.checkRequiredDependencies(task = this)
val runner = createRunner() val runner = createRunner()