srs / gradle-node-plugin

Gradle plugin for integrating NodeJS in your build. :rocket:
Apache License 2.0
866 stars 211 forks source link

npmInstall task fails if broken symlink in node_modules directory #202

Open sirianni opened 7 years ago

sirianni commented 7 years ago

Even though npm install succeeds, the gradle build fails with:

$ gradle --stacktrace :webapp:npmInstall        
:webapp:nodeSetup SKIPPED
:webapp:npmSetup SKIPPED
:webapp:npmInstall

FAILURE: Build failed with an exception.

* What went wrong:
Could not list contents of '..../webapp/node_modules/.bin/user-home'. Couldn't follow symbolic link.

* Try:
Run with --info or --debug option to get more log output.

* Exception is:
org.gradle.api.GradleException: Could not list contents of '/home/esirianni/src/support/infosight/portal/webapp/node_modules/.bin/user-home'. Couldn't follow symbolic link.

Sure enough, the symlink is broken:

$ ls -l node_modules/.bin/user-home
lrwxrwxrwx 1 esirianni support-dept 19 Mar  3 10:35 node_modules/.bin/user-home -> ../user-home/cli.js

However, I'm not sure why gradle (or the gradle-node-plugin) cares. My guess is somehow the plugin is requesting that the entire node_modules directory be fingerprinted for some sort of "up to date" checking? Can this be disabled?

Here is the full stacktrace:

        at org.gradle.api.internal.file.collections.jdk7.Jdk7DirectoryWalker$1.visitFile(Jdk7DirectoryWalker.java:79)
        at org.gradle.api.internal.file.collections.jdk7.Jdk7DirectoryWalker$1.visitFile(Jdk7DirectoryWalker.java:54)
        at org.gradle.api.internal.file.collections.jdk7.Jdk7DirectoryWalker.walkDir(Jdk7DirectoryWalker.java:54)
        at org.gradle.api.internal.file.collections.DirectoryFileTree.walkDir(DirectoryFileTree.java:146)
        at org.gradle.api.internal.file.collections.DirectoryFileTree.visitFrom(DirectoryFileTree.java:130)
        at org.gradle.api.internal.file.collections.DirectoryFileTree.visit(DirectoryFileTree.java:115)
        at org.gradle.api.internal.file.collections.DirectoryFileTree.visitTreeOrBackingFile(DirectoryFileTree.java:111)
        at org.gradle.api.internal.file.collections.FileTreeAdapter.visitTreeOrBackingFile(FileTreeAdapter.java:114)
        at org.gradle.api.internal.changedetection.state.DefaultFileCollectionSnapshotter.visitFiles(DefaultFileCollectionSnapshotter.java:42)
        at org.gradle.api.internal.changedetection.state.AbstractFileCollectionSnapshotter.snapshot(AbstractFileCollectionSnapshotter.java:62)
        at org.gradle.api.internal.changedetection.state.DefaultFileCollectionSnapshotter.snapshot(DefaultFileCollectionSnapshotter.java:31)
        at org.gradle.api.internal.changedetection.state.AbstractFileCollectionSnapshotter.snapshot(AbstractFileCollectionSnapshotter.java:100)
        at org.gradle.api.internal.changedetection.state.DefaultFileCollectionSnapshotter.snapshot(DefaultFileCollectionSnapshotter.java:31)
        at org.gradle.api.internal.changedetection.state.OutputFilesCollectionSnapshotter.snapshot(OutputFilesCollectionSnapshotter.java:60)
        at org.gradle.api.internal.changedetection.rules.AbstractNamedFileSnapshotTaskStateChanges.buildSnapshots(AbstractNamedFileSnapshotTaskStateChanges.java:85)
        at org.gradle.api.internal.changedetection.rules.AbstractNamedFileSnapshotTaskStateChanges.<init>(AbstractNamedFileSnapshotTaskStateChanges.java:53)
        at org.gradle.api.internal.changedetection.rules.OutputFilesTaskStateChanges.<init>(OutputFilesTaskStateChanges.java:31)
        at org.gradle.api.internal.changedetection.rules.TaskUpToDateState.<init>(TaskUpToDateState.java:54)
        at org.gradle.api.internal.changedetection.changes.DefaultTaskArtifactStateRepository$TaskArtifactStateImpl.getStates(DefaultTaskArtifactStateRepository.java:162)
        at org.gradle.api.internal.changedetection.changes.DefaultTaskArtifactStateRepository$TaskArtifactStateImpl.isUpToDate(DefaultTaskArtifactStateRepository.java:82)
        at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:52)
        at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:58)
        at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:52)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:53)
        at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
        at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:233)
        at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:215)
        at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.processTask(AbstractTaskPlanExecutor.java:74)
        at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.run(AbstractTaskPlanExecutor.java:55)
        at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor.process(DefaultTaskPlanExecutor.java:32)
        at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter.execute(DefaultTaskGraphExecuter.java:113)
        at org.gradle.execution.SelectedTaskExecutionAction.execute(SelectedTaskExecutionAction.java:37)
        at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:37)
        at org.gradle.execution.DefaultBuildExecuter.access$000(DefaultBuildExecuter.java:23)
        at org.gradle.execution.DefaultBuildExecuter$1.proceed(DefaultBuildExecuter.java:43)
        at org.gradle.execution.DryRunBuildExecutionAction.execute(DryRunBuildExecutionAction.java:32)
        at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:37)
        at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:30)
        at org.gradle.initialization.DefaultGradleLauncher$4.run(DefaultGradleLauncher.java:186)
        at org.gradle.internal.Factories$1.create(Factories.java:22)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:91)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:53)
        at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:183)
        at org.gradle.initialization.DefaultGradleLauncher.access$200(DefaultGradleLauncher.java:33)
        at org.gradle.initialization.DefaultGradleLauncher$1.create(DefaultGradleLauncher.java:112)
        at org.gradle.initialization.DefaultGradleLauncher$1.create(DefaultGradleLauncher.java:106)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:91)
        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:63)
        at org.gradle.initialization.DefaultGradleLauncher.doBuild(DefaultGradleLauncher.java:106)
        at org.gradle.initialization.DefaultGradleLauncher.run(DefaultGradleLauncher.java:92)
        at org.gradle.launcher.exec.GradleBuildController.run(GradleBuildController.java:66)
        at org.gradle.tooling.internal.provider.ExecuteBuildActionRunner.run(ExecuteBuildActionRunner.java:28)
        at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)
        at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:41)
        at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:26)
        at org.gradle.tooling.internal.provider.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:79)
        at org.gradle.tooling.internal.provider.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:51)
        at org.gradle.launcher.daemon.server.exec.ExecuteBuild.doBuild(ExecuteBuild.java:59)
        at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
        at org.gradle.launcher.daemon.server.exec.WatchForDisconnection.execute(WatchForDisconnection.java:47)
        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
        at org.gradle.launcher.daemon.server.exec.ResetDeprecationLogger.execute(ResetDeprecationLogger.java:26)
        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
        at org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon.execute(RequestStopIfSingleUsedDaemon.java:34)
        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
        at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:74)
        at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:72)
        at org.gradle.util.Swapper.swap(Swapper.java:38)
        at org.gradle.launcher.daemon.server.exec.ForwardClientInput.execute(ForwardClientInput.java:72)
        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
        at org.gradle.launcher.daemon.server.exec.LogAndCheckHealth.execute(LogAndCheckHealth.java:55)
        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
        at org.gradle.launcher.daemon.server.exec.LogToClient.doBuild(LogToClient.java:60)
        at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
        at org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment.doBuild(EstablishBuildEnvironment.java:72)
        at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
        at org.gradle.launcher.daemon.server.exec.HintGCAfterBuild.execute(HintGCAfterBuild.java:44)
        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
        at org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy$1.run(StartBuildOrRespondWithBusy.java:50)
        at org.gradle.launcher.daemon.server.DaemonStateCoordinator$1.run(DaemonStateCoordinator.java:293)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:54)
        at org.gradle.internal.concurrent.StoppableExecutorImpl$1.run(StoppableExecutorImpl.java:40)
sirianni commented 7 years ago

This seems to be the culprit:

getOutputs().dir( new File( (File) this.project.node.nodeModulesDir, 'node_modules' ) )

Is there any harm in removing this?

jeysal commented 7 years ago

Removing this would cause the npmInstall task to skip execution despite the node_modules dir having been altered (e.g. deleted).

It should be possible to disable the output caching via npmInstall.outputs.doNotCacheIf('broken symlink') { true }, but I tried it and Gradle still attempts to read the node_modules dir.

You could run the rule-generated npm_install task instead, and specify only the inputs:

npm_install.inputs.file 'package.json'
npm_install.outputs.upToDateWhen { true }
sirianni commented 7 years ago
npm_install.outputs.upToDateWhen { true }

Shouldn't that be false?

sirianni commented 7 years ago

Here is what I ended up doing:

task npmInstall(overwrite: true, type: NpmTask, dependsOn: 'npmSetup') {
    npmCommand = 'install'
    inputs.file('package.json')
}
jeysal commented 7 years ago

npm_install.outputs.upToDateWhen { true } That would cause the task to execute every single time, which I would consider extremely annoying due to its impact on build execution time.

The way I suggested, npm_install is only rerun when you change the package.json. Of course that could potentially break the build if you delete the node_modules folder, but occasionally using --rerun-tasks (or manually executing npm i) is probably better than running the npm install on every build, at least for performance.

jeysal commented 7 years ago

task npmInstall(overwrite: true, type: NpmTask, dependsOn: 'npmSetup')

Overwriting the npmInstall task is, of course, also an option instead of using npm_install.

Regarding the outputs, I hope I outlined the options clearly in my comment above so you can decide what's worth more to you - performance (settings the outputs to always up-to-date to avoid npm installing every time) or build stability (omitting the outputs).

sirianni commented 7 years ago

Yes, thanks for clarifying.

I had incorrectly assumed that npm install itself was cheap if nothing had changed. Now that I understand it better, I have opted for the outputs.upToDateWhen { true } as you suggested.

sirianni commented 7 years ago

I ended up going with this:

task npmInstall(overwrite: true, type: NpmTask, dependsOn: 'npmSetup') {
    inputs.file('package.json')
    outputs.upToDateWhen { file('node_modules').exists() }

    npmCommand = ['install']
}

This supports a common use case when developers nuke the node_modules directory before triggering rebuild.

jeysal commented 7 years ago

I had incorrectly assumed that npm install itself was cheap if nothing had changed

Yes, I'm still wondering what the heck npm is doing in that case every time ;)

outputs.upToDateWhen { file('node_modules').exists() }

That's a nice workaround!

BTW, here's the relevant Gradle issue dealing with the dangling symlinks as outputs.

sirianni commented 7 years ago

Note to others, that setting npmCommand as an array is critical. Without the enclosing brackets, groovy seems to coerce 'install' into ['i', 'n', 's', 't', 'a', 'l', 'l']. Which ends up npm installing the packages n, s, t, and a instead of what is in your package.json. This took me quite a while to debug

chetanbaheti commented 7 years ago

Unfortunately the workaround provided @sirianni did not work for me. Based on comments in the original gradle issue, I added a method in the script to delete bad sym links which fixed the issue for us. Relevant code below:

import java.nio.file.Files
import java.nio.file.Path

ext.deleteInvalidSymLinks = {dir ->
    println ("deleting bad links in $dir")
    Files.walk(dir.toPath())
    .filter { it -> 
        logger.debug("file: " + it + ", symLink?:" + Files.isSymbolicLink(it))
        def isInvalidLink = Files.isSymbolicLink(it) && !Files.exists(it)
        return isInvalidLink;
     }
    .forEach{ it ->
        try {
            logger.warn("Deleting broken symlink '$it'")
            Files.delete(it)
        } catch (Exception e) {
            logger.error("Couldn't delete broken symlink '$it' : $e ")
        }
    }
}

npmInstall.doLast {
    deleteInvalidSymLinks(project.file('node_modules'))
}