Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
yevhensayenko opened this issue Oct 21, 2022 · 6 comments · Fixed by #1500
Closed
Assignees

Comments

@yevhensayenko
Copy link

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.

@yevhensayenko yevhensayenko 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 Oct 21, 2022
@simschla simschla self-assigned this Jan 5, 2023
@simschla
Copy link
Contributor

simschla commented Jan 12, 2023

@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?

@simschla
Copy link
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 ?

@nedtwigg
Copy link
Member

I agree that option 2 is clearer.

@jnels124
Copy link

jnels124 commented Jan 17, 2023

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()
    }
}

@simschla
Copy link
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.

@nedtwigg
Copy link
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