From fbefa17337ada8e29cf41f7127b5aefec6aaef82 Mon Sep 17 00:00:00 2001 From: Sergey Rostov Date: Fri, 31 May 2019 13:07:00 +0300 Subject: [PATCH] Gradle, NPM, windows: creating symlink without administrator privilege #KT-31694 Fixed --- .../gradle/targets/js/npm/NpmSimpleLinker.kt | 66 +++++++++++++++++-- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmSimpleLinker.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmSimpleLinker.kt index a31f29af514..b5831efbb84 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmSimpleLinker.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/js/npm/NpmSimpleLinker.kt @@ -5,15 +5,19 @@ package org.jetbrains.kotlin.gradle.targets.js.npm +import com.google.gson.stream.JsonWriter import org.gradle.api.Project import org.jetbrains.kotlin.gradle.targets.js.nodejs.nodeJs +import java.io.ByteArrayInputStream +import java.io.File +import java.io.StringWriter import java.nio.file.Files /** * Handles simple case, when there are no npm depenencies required, but some tasks steel needs * node_modules with symlinked packages and packages_imported */ -class NpmSimpleLinker(rootProject: Project) { +class NpmSimpleLinker(val rootProject: Project) { private val rootProjectNodeModules = rootProject.nodeJs.root.rootPackageDir.resolve(NpmProject.NODE_MODULES) fun link(projects: Collection) { @@ -29,19 +33,71 @@ class NpmSimpleLinker(rootProject: Project) { rootProjectNodeModules.mkdirs() + val fsLinker = createFSLinker() + // packages projects.forEach { - Files.createSymbolicLink(getNodeModulePath(it.npmProject.name), it.npmProject.dir.canonicalFile.toPath()) + fsLinker.link(getNodeModulePath(it.npmProject.name), it.npmProject.dir.canonicalFile) } // packages_imported projects.flatMapTo(mutableSetOf()) { it.gradleDependencies.externalModules.map { gradleNodeModule -> gradleNodeModule } }.forEach { - Files.createSymbolicLink(getNodeModulePath(it.name), it.path.canonicalFile.toPath()) + fsLinker.link(getNodeModulePath(it.name), it.path.canonicalFile) + } + + fsLinker.execute() + } + + private fun getNodeModulePath(name: String) = rootProjectNodeModules.resolve(name).canonicalFile + + interface FSLinker { + fun link(linkFile: File, target: File) + fun execute() = Unit + } + + fun createFSLinker(): FSLinker = + if (rootProject.nodeJs.root.environment.isWindows) WindowsJunctionFsLinker(rootProject, rootProjectNodeModules) + else JavaFsLinker() + + class WindowsJunctionFsLinker(val project: Project, val dir: File) : FSLinker { + val js = StringBuilder() + + init { + js.appendln("var fs = require('fs')") + } + + fun addLink(linkFile: String, target: String) { + js.appendln("fs.symlinkSync(${jsQuotedString(linkFile)}, ${jsQuotedString(target)}, 'junction')") + } + + private fun jsQuotedString(str: String) = StringWriter().also { + JsonWriter(it).value(str) + }.toString() + + override fun link(linkFile: File, target: File) { + addLink(linkFile.absolutePath, target.absolutePath) + } + + override fun execute() { + val result = project.exec { exec -> + exec.workingDir = dir + exec.executable = project.nodeJs.root.environment.nodeExecutable + exec.standardInput = ByteArrayInputStream(js.toString().toByteArray()) + exec.standardOutput = System.`out` + exec.errorOutput = System.err + } + + if (result.exitValue != 0) { + error("Cannot create window junctions: nodejs exited with ${result.exitValue}") + } } } - private fun getNodeModulePath(name: String) = - rootProjectNodeModules.resolve(name).canonicalFile.toPath() + class JavaFsLinker : FSLinker { + override fun link(linkFile: File, target: File) { + Files.createSymbolicLink(linkFile.toPath(), target.toPath()) + } + } } \ No newline at end of file