Skip to content

Feature request: Add ability to extend PATH environment variable for the npm process #1381

Description

@yevhensayenko

I would like to use local nodeJS, that is downloaded into project dir using https://github.com/node-gradle/gradle-node-plugin. I tried to specify npm executable using using prettier().npmExecutable('<path to project>/.gradle/nodejs/node-v16.18.0-linux-x64/bin/npm'). But it still expects node command to be present. So the only way to set it up for me was to override PATH using command line like PATH=/<path to project>/.gradle/nodejs/node-v16.18.0-linux-x64/bin:$PATH ./gradlew.

But it seems that we could do it by overriding PATH variable using java.lang.ProcessBuilder#environment() in com.diffplug.spotless.npm.NpmProcess#npm. So, I think it would be helpful to be able to set up node bin directory in settings, so it can be then added into PATH environment variable for the process.

Activity

  1. changed the title [-]Feature request: Add ability to specify PATH environment variable for the npm process[/-] [+]Feature request: Add ability to extend PATH environment variable for the npm process[/+] on Oct 21, 2022
  2. self-assigned this
    on Jan 5, 2023
  3. simschla commented on Jan 12, 2023

    @simschla
    Contributor

    @yevhensayenko I'm trying to reproduce your problem, but it works for me when I'm using the node plugin and specify that npm using npmExecutable(...)

    Can you provide extracts from your build file regarding spotless and node? Maybe I'm doing something differently?
    Also: which spotless version are you using?

  4. simschla commented on Jan 14, 2023

    @simschla
    Contributor

    I'm currently experimenting with an API like this (Option 1):

    prettier()
        .npmExecutable('/path/to/npm')
        .npmEnv(['PATH': '/path/to/node-dir' + ':' + System.getenv('PATH')])

    This would allow for passing various env-vars to the node process and would also solve the gradle-node-plugin requirements. But I'm unsure if this is the way to go, as this api seems to be a bit too "open"?

    Maybe it would be sufficient to offer an API like this (Option 2):

    prettier()
        .npmExecutable('/path/to/npm')
        .nodeExecutable('/path/to/node')

    -> I would then internally use the nodeExecutable-path to add it to the PATH-env variable passed to the node process.
    This would also solve the gradle-node-plugin issue, but keeps the API tighter.

    I'm leaning toward Option 2 as it is cleaner and fits better with the current API, but would like to hear your thoughts and comments @nedtwigg and @yevhensayenko ?

  5. nedtwigg commented on Jan 14, 2023

    @nedtwigg
    Member

    I agree that option 2 is clearer.

  6. jnels124 commented on Jan 17, 2023

    @jnels124

    Just a small note. This will not actually solve the use of the gradle node plugin. That plugin needs to use a task in order to install node. Since prettier is evaluated at configuration time, and attempt to do the npm install at this time, the node gradle plugin can't resolve the issue. Below is an example of using the objects of the node gradle plugin to install at configuration time. Maybe it would be best to have a SetupPrettier task that registerInternalDependencies (or some other task) that verifies npm and does the npm install instead of doing this at configuration time.

    package plugin.nodejs
    
    import com.github.gradle.node.NodeExtension
    import com.github.gradle.node.util.PlatformHelper
    import com.github.gradle.node.util.ProjectApiHelper
    import com.github.gradle.node.variant.VariantComputer
    
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    import org.gradle.api.file.Directory
    import org.gradle.api.file.RegularFile
    import org.gradle.api.provider.Provider
    import org.gradle.util.GradleVersion
    import java.io.File
    import java.nio.file.Files
    import java.nio.file.Path
    import java.nio.file.Paths
    
    class NodePlugin : Plugin<Project> {
        override fun apply(project: Project) {
            val nodeExtension = NodeExtension.create(project)
    //        val nodeExtension = project.extensions.create<NodeExtension>("nodeWrapper", project)
            project.task("hello") {
                doLast{
                    println(nodeExtension.version);
                }
            }
    
            if (nodeExtension.download.get()) {
                nodeExtension.distBaseUrl.orNull?.let { addRepository(it, nodeExtension.allowInsecureProtocol.orNull, project) }
                val platformHelper = PlatformHelper.INSTANCE
                val variantComputer = VariantComputer(platformHelper)
                val nodeArchiveDependencyProvider = variantComputer.computeNodeArchiveDependency(nodeExtension)
                val archiveFileProvider = nodeArchiveDependencyProvider
                    .map { nodeArchiveDependency ->
                        resolveNodeArchiveFile(nodeArchiveDependency, project)
                    }
                val nodeArchiveFile = project.objects.fileProperty();
                nodeArchiveFile.set(project.layout.file(archiveFileProvider))
                unpackNodeArchive(variantComputer, nodeExtension, nodeArchiveFile, project)
                setExecutableFlag(platformHelper, variantComputer, nodeExtension);
            }
        }
    
        private fun unpackNodeArchive(variantComputer: VariantComputer, nodeExtension: NodeExtension, nodeArchiveFile: Provider<RegularFile>, project: Project) {
            val archiveFile = nodeArchiveFile.get().asFile
            val nodeDirProvider = variantComputer.computeNodeDir(nodeExtension)
            if (nodeDirProvider.get().asFile.exists()) return;
    
            val nodeBinDirProvider = variantComputer.computeNodeBinDir(nodeDirProvider)
            val archivePath = nodeDirProvider.map { it.dir("../") }
            val projectHelper = ProjectApiHelper.newInstance(project)
            System.out.println(String.format("archiveFile=%s nodeDirProvider=%s nodeBinProvider=%s archivePath=%s",
                archiveFile.toString(),
                nodeDirProvider.map { it }.get(),
                nodeBinDirProvider.map { it }.get(),
                archivePath.get()))
            if (archiveFile.name.endsWith("zip")) {
                projectHelper.copy {
                    from(projectHelper.zipTree(archiveFile))
                    into(archivePath)
                }
            } else {
                projectHelper.copy {
                    from(projectHelper.tarTree(archiveFile))
                    into(archivePath)
                }
                // Fix broken symlink
                val nodeBinDirPath = nodeBinDirProvider.get().asFile.toPath()
                fixBrokenSymlink("npm", nodeBinDirPath, nodeDirProvider, variantComputer)
                fixBrokenSymlink("npx", nodeBinDirPath, nodeDirProvider, variantComputer)
            }
        }
    
        private fun fixBrokenSymlink(name: String, nodeBinDirPath: Path, nodeDirProvider: Provider<Directory>, variantComputer: VariantComputer) {
            val script = nodeBinDirPath.resolve(name)
            val scriptFile = variantComputer.computeNpmScriptFile(nodeDirProvider, name).get()
            System.out.println(String.format("The script is %s the script file is %s", script, scriptFile));
            if (Files.deleteIfExists(script)) {
                System.out.println("Deleting script " + script)
                Files.createSymbolicLink(script, nodeBinDirPath.relativize(Paths.get(scriptFile)))
            }
        }
    
        private fun setExecutableFlag(platformHelper: PlatformHelper, variantComputer: VariantComputer, nodeExtension: NodeExtension) {
            if (!platformHelper.isWindows) {
                val nodeDirProvider = variantComputer.computeNodeDir(nodeExtension)
                val nodeBinDirProvider = variantComputer.computeNodeBinDir(nodeDirProvider)
                val nodeExecProvider = variantComputer.computeNodeExec(nodeExtension, nodeBinDirProvider)
                File(nodeExecProvider.get()).setExecutable(true)
            }
        }
    
        private fun addRepository(distUrl: String, allowInsecureProtocol: Boolean?, project: Project) {
            System.out.println(String.format("The distUrl=%s", distUrl))
            project.repositories.ivy {
                name = "Node.js"
                setUrl(distUrl)
                patternLayout {
                    artifact("v[revision]/[artifact](-v[revision]-[classifier]).[ext]")
                }
                metadataSources {
                    artifact()
                }
                content {
                    includeModule("org.nodejs", "node")
                }
                if (GradleVersion.current() >= GradleVersion.version("6.0")) {
                    allowInsecureProtocol?.let { isAllowInsecureProtocol = it }
                }
            }
        }
    
        private fun resolveNodeArchiveFile(name: String, project: Project): File {
            val dependency = project.dependencies.create(name)
            val configuration = project.configurations.detachedConfiguration(dependency)
            configuration.isTransitive = false
    
            return configuration.resolve().single()
        }
    }
    
  7. simschla commented on Jan 19, 2023

    @simschla
    Contributor

    Currently one needs to first setup node (using a gradle call like gradlew nodeSetup npmSetup and then invoke spotless 'gradlew spotlessApply` ->,linking the spotless tasks to the setup-tasks does not work as of now. We will need to resolve #1499 for this. Feel free to follow that issue to be notified of full support.

  8. nedtwigg commented on Jan 26, 2023

    @nedtwigg
    Member

    Released in plugin-gradle 6.14.0 and plugin-maven 2.31.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions