Skip to content

Commit

Permalink
add the ability to explictly specify the path to the npm executable
Browse files Browse the repository at this point in the history
  • Loading branch information
dconeybe committed Nov 19, 2024
1 parent 510f258 commit a66e0ae
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 2 deletions.
4 changes: 4 additions & 0 deletions dataconnect/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@
local.properties
.dataconnect/
.firebaserc

# The file used by our custom gradle plugin to specify _local_ settings.
# Since the settings are "local", they should *not* be checked into source control.
dataconnect.local.toml
9 changes: 9 additions & 0 deletions dataconnect/buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,22 @@
plugins {
// See https://docs.gradle.org/current/userguide/kotlin_dsl.html#sec:kotlin-dsl_plugin
`kotlin-dsl`
kotlin("plugin.serialization") version embeddedKotlinVersion
}

java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) } }

dependencies {
implementation(libs.android.gradlePlugin.api)
implementation(libs.snakeyaml)

// TODO: Upgrade the `tomlkt` dependency to 0.4.0 or later once the gradle
// wrapper version used by this project uses a sufficiently-recent version
// of kotlin. At the time of writing, `embeddedKotlinVersion` is 1.9.22,
// which requires an older version of `tomlkt` because the newer versions
// depend on a newer version of the `kotlinx.serialization` plugin, which
// requires a newer version of Kotlin.
implementation("net.peanuuutz.tomlkt:tomlkt:0.3.7")
}

gradlePlugin {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.example.dataconnect.gradle;

import org.gradle.api.Transformer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

// TODO: Remove this interface and use Transformer directly once the Kotlin
// version is upgraded to a later version that doesn't require it, such as
// 1.9.25. At the time of writing, the Kotlin version in use is 1.9.22.
//
// Using this interface works around the following Kotlin compiler error:
//
// > Task :plugin:compileKotlin FAILED
// e: DataConnectGradlePlugin.kt:93:15 Type mismatch: inferred type is RegularFile? but TypeVariable(S) was expected
// e: DataConnectGradlePlugin.kt:102:15 Type mismatch: inferred type is String? but TypeVariable(S) was expected
// e: DataConnectGradlePlugin.kt:111:15 Type mismatch: inferred type is DataConnectExecutable.VerificationInfo? but TypeVariable(S) was expected
public interface TransformerInterop<OUT, IN> extends Transformer<OUT, IN> {

@Override
@Nullable OUT transform(@NotNull IN in);

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,25 @@ package com.google.firebase.example.dataconnect.gradle

import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction

abstract class FirebaseToolsSetupTask : DefaultTask() {

@get:Input abstract val version: Property<String>

@get:InputFile @get:Optional
abstract val npmExecutable: Property<File>

@get:OutputDirectory abstract val outputDirectory: DirectoryProperty

@get:Internal
Expand All @@ -40,9 +46,11 @@ abstract class FirebaseToolsSetupTask : DefaultTask() {
@TaskAction
fun run() {
val version: String = version.get()
val npmExecutable: File? = npmExecutable.orNull
val outputDirectory: File = outputDirectory.get().asFile

logger.info("version: {}", version)
logger.info("npmExecutable: {}", npmExecutable?.absolutePath)
logger.info("outputDirectory: {}", outputDirectory.absolutePath)

project.delete(outputDirectory)
Expand All @@ -52,13 +60,35 @@ abstract class FirebaseToolsSetupTask : DefaultTask() {
packageJsonFile.writeText("{}", Charsets.UTF_8)

runCommand(File(outputDirectory, "install.log.txt")) {
commandLine("npm", "install", "firebase-tools@$version")
val arg0 = npmExecutable?.absolutePath ?: "npm"
commandLine(arg0, "install", "firebase-tools@$version")
workingDir(outputDirectory)
}
}

internal fun configureFrom(providers: MyProjectProviders) {
version.set(providers.firebaseToolsVersion)
outputDirectory.set(providers.buildDirectory.map { it.dir("firebase-tools") })

npmExecutable.set(
providers.localConfigs.map(
TransformerInterop { localConfigs ->
val result = localConfigs.filter {
it.npmExecutable !== null
}.map { Pair(it.srcFile, it.npmExecutable!!) }.firstOrNull()
result?.let { (configFile, npmExecutablePath) ->
File(npmExecutablePath).also {
if (!it.exists()) {
throw GradleException(
"npmExecutable specified in ${configFile?.absolutePath} " +
"does not exist: ${it.absolutePath} " +
"(error code eaw5gppkep)"
)
}
}
}
}
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.example.dataconnect.gradle

import java.io.File
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

@Serializable
internal data class LocalConfig(val npmExecutable: String? = null, @Transient val srcFile: File? = null) :
java.io.Serializable {
companion object {
@Suppress("ConstPropertyName")
private const val serialVersionUID = 6103369922496556758L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
package com.google.firebase.example.dataconnect.gradle

import com.android.build.api.variant.ApplicationVariant
import java.io.FileNotFoundException
import javax.inject.Inject
import net.peanuuutz.tomlkt.Toml
import net.peanuuutz.tomlkt.decodeFromString
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.file.Directory
Expand All @@ -32,6 +35,7 @@ import org.gradle.kotlin.dsl.newInstance
internal open class MyProjectProviders(
projectBuildDirectory: DirectoryProperty,
providerFactory: ProviderFactory,
projectDirectoryHierarchy: List<Directory>,
ext: DataConnectExtension
) {

Expand All @@ -42,6 +46,7 @@ internal open class MyProjectProviders(
) : this(
projectBuildDirectory = project.layout.buildDirectory,
providerFactory = project.providers,
projectDirectoryHierarchy = project.projectDirectoryHierarchy(),
ext = project.extensions.getByType<DataConnectExtension>()
)

Expand All @@ -51,10 +56,54 @@ internal open class MyProjectProviders(
providerFactory.provider {
ext.firebaseToolsVersion
?: throw GradleException(
"dataconnect.firebaseToolsVersion must be set in your build.gradle or build.gradle.kts " +
"dataconnect.firebaseToolsVersion must be set in your " +
"build.gradle or build.gradle.kts " +
"(error code xbmvkc3mtr)"
)
}

val localConfigFiles: Provider<List<RegularFile>> = providerFactory.provider {
projectDirectoryHierarchy.map { it.file("dataconnect.local.toml") }
}

val localConfigs: Provider<List<LocalConfig>> = run {
val lazyResult = lazy(LazyThreadSafetyMode.PUBLICATION) {
projectDirectoryHierarchy
.map { it.file("dataconnect.local.toml").asFile }
.mapNotNull { file ->
val text = file.runCatching { readText() }.fold(
onSuccess = { it },
onFailure = { exception ->
if (exception is FileNotFoundException) {
null // ignore non-existent config files
} else {
throw GradleException(
"reading file failed: ${file.absolutePath} ($exception)" +
" (error code bj7dxvvw5p)",
exception
)
}
}
)
if (text === null) null else Pair(file, text)
}.map { (file, text) ->
val toml = Toml { this.ignoreUnknownKeys = true }
toml.runCatching {
decodeFromString<LocalConfig>(text, "dataconnect").copy(srcFile = file)
}.fold(
onSuccess = { it },
onFailure = { exception ->
throw GradleException(
"parsing toml file failed: ${file.absolutePath} ($exception)" +
" (error code 44dkc2vvpq)",
exception
)
}
)
}
}
providerFactory.provider { lazyResult.value }
}
}

internal open class MyVariantProviders(
Expand Down Expand Up @@ -105,3 +154,11 @@ private val Project.firebaseToolsSetupTask: FirebaseToolsSetupTask
}
return tasks.single()
}

private fun Project.projectDirectoryHierarchy(): List<Directory> = buildList {
var curProject: Project? = this@projectDirectoryHierarchy
while (curProject !== null) {
add(curProject.layout.projectDirectory)
curProject = curProject.parent
}
}
10 changes: 10 additions & 0 deletions dataconnect/dataconnect.local.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[dataconnect]

# The path of the "npm" executable to use to install firebase-tools.
# Setting this is normally not necessary; however, if "npm" is not in the global
# PATH, or the wrong version is in the global PATH, then setting this to the absolute
# path of the npm executable to use works around that problem. Setting it to null
# uses "npm" as found in the PATH.
#
# eg. npmExecutable = "/home/myusername/local/nvm/versions/node/v20.13.1/bin/npm"
npmExecutable = null

0 comments on commit a66e0ae

Please sign in to comment.