Skip to content

Commit a66e0ae

Browse files
committed
add the ability to explictly specify the path to the npm executable
1 parent 510f258 commit a66e0ae

File tree

7 files changed

+179
-2
lines changed

7 files changed

+179
-2
lines changed

dataconnect/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@
1515
local.properties
1616
.dataconnect/
1717
.firebaserc
18+
19+
# The file used by our custom gradle plugin to specify _local_ settings.
20+
# Since the settings are "local", they should *not* be checked into source control.
21+
dataconnect.local.toml

dataconnect/buildSrc/build.gradle.kts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,22 @@
1717
plugins {
1818
// See https://docs.gradle.org/current/userguide/kotlin_dsl.html#sec:kotlin-dsl_plugin
1919
`kotlin-dsl`
20+
kotlin("plugin.serialization") version embeddedKotlinVersion
2021
}
2122

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

2425
dependencies {
2526
implementation(libs.android.gradlePlugin.api)
2627
implementation(libs.snakeyaml)
28+
29+
// TODO: Upgrade the `tomlkt` dependency to 0.4.0 or later once the gradle
30+
// wrapper version used by this project uses a sufficiently-recent version
31+
// of kotlin. At the time of writing, `embeddedKotlinVersion` is 1.9.22,
32+
// which requires an older version of `tomlkt` because the newer versions
33+
// depend on a newer version of the `kotlinx.serialization` plugin, which
34+
// requires a newer version of Kotlin.
35+
implementation("net.peanuuutz.tomlkt:tomlkt:0.3.7")
2736
}
2837

2938
gradlePlugin {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.firebase.example.dataconnect.gradle;
17+
18+
import org.gradle.api.Transformer;
19+
import org.jetbrains.annotations.NotNull;
20+
import org.jetbrains.annotations.Nullable;
21+
22+
// TODO: Remove this interface and use Transformer directly once the Kotlin
23+
// version is upgraded to a later version that doesn't require it, such as
24+
// 1.9.25. At the time of writing, the Kotlin version in use is 1.9.22.
25+
//
26+
// Using this interface works around the following Kotlin compiler error:
27+
//
28+
// > Task :plugin:compileKotlin FAILED
29+
// e: DataConnectGradlePlugin.kt:93:15 Type mismatch: inferred type is RegularFile? but TypeVariable(S) was expected
30+
// e: DataConnectGradlePlugin.kt:102:15 Type mismatch: inferred type is String? but TypeVariable(S) was expected
31+
// e: DataConnectGradlePlugin.kt:111:15 Type mismatch: inferred type is DataConnectExecutable.VerificationInfo? but TypeVariable(S) was expected
32+
public interface TransformerInterop<OUT, IN> extends Transformer<OUT, IN> {
33+
34+
@Override
35+
@Nullable OUT transform(@NotNull IN in);
36+
37+
}

dataconnect/buildSrc/src/main/kotlin/com/google/firebase/example/dataconnect/gradle/FirebaseToolsSetupTask.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,25 @@ package com.google.firebase.example.dataconnect.gradle
1818

1919
import java.io.File
2020
import org.gradle.api.DefaultTask
21+
import org.gradle.api.GradleException
2122
import org.gradle.api.file.DirectoryProperty
2223
import org.gradle.api.file.RegularFile
2324
import org.gradle.api.provider.Property
2425
import org.gradle.api.provider.Provider
2526
import org.gradle.api.tasks.Input
27+
import org.gradle.api.tasks.InputFile
2628
import org.gradle.api.tasks.Internal
29+
import org.gradle.api.tasks.Optional
2730
import org.gradle.api.tasks.OutputDirectory
2831
import org.gradle.api.tasks.TaskAction
2932

3033
abstract class FirebaseToolsSetupTask : DefaultTask() {
3134

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

37+
@get:InputFile @get:Optional
38+
abstract val npmExecutable: Property<File>
39+
3440
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
3541

3642
@get:Internal
@@ -40,9 +46,11 @@ abstract class FirebaseToolsSetupTask : DefaultTask() {
4046
@TaskAction
4147
fun run() {
4248
val version: String = version.get()
49+
val npmExecutable: File? = npmExecutable.orNull
4350
val outputDirectory: File = outputDirectory.get().asFile
4451

4552
logger.info("version: {}", version)
53+
logger.info("npmExecutable: {}", npmExecutable?.absolutePath)
4654
logger.info("outputDirectory: {}", outputDirectory.absolutePath)
4755

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

5462
runCommand(File(outputDirectory, "install.log.txt")) {
55-
commandLine("npm", "install", "firebase-tools@$version")
63+
val arg0 = npmExecutable?.absolutePath ?: "npm"
64+
commandLine(arg0, "install", "firebase-tools@$version")
5665
workingDir(outputDirectory)
5766
}
5867
}
5968

6069
internal fun configureFrom(providers: MyProjectProviders) {
6170
version.set(providers.firebaseToolsVersion)
6271
outputDirectory.set(providers.buildDirectory.map { it.dir("firebase-tools") })
72+
73+
npmExecutable.set(
74+
providers.localConfigs.map(
75+
TransformerInterop { localConfigs ->
76+
val result = localConfigs.filter {
77+
it.npmExecutable !== null
78+
}.map { Pair(it.srcFile, it.npmExecutable!!) }.firstOrNull()
79+
result?.let { (configFile, npmExecutablePath) ->
80+
File(npmExecutablePath).also {
81+
if (!it.exists()) {
82+
throw GradleException(
83+
"npmExecutable specified in ${configFile?.absolutePath} " +
84+
"does not exist: ${it.absolutePath} " +
85+
"(error code eaw5gppkep)"
86+
)
87+
}
88+
}
89+
}
90+
}
91+
)
92+
)
6393
}
6494
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.example.dataconnect.gradle
18+
19+
import java.io.File
20+
import kotlinx.serialization.Serializable
21+
import kotlinx.serialization.Transient
22+
23+
@Serializable
24+
internal data class LocalConfig(val npmExecutable: String? = null, @Transient val srcFile: File? = null) :
25+
java.io.Serializable {
26+
companion object {
27+
@Suppress("ConstPropertyName")
28+
private const val serialVersionUID = 6103369922496556758L
29+
}
30+
}

dataconnect/buildSrc/src/main/kotlin/com/google/firebase/example/dataconnect/gradle/Providers.kt

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
package com.google.firebase.example.dataconnect.gradle
1818

1919
import com.android.build.api.variant.ApplicationVariant
20+
import java.io.FileNotFoundException
2021
import javax.inject.Inject
22+
import net.peanuuutz.tomlkt.Toml
23+
import net.peanuuutz.tomlkt.decodeFromString
2124
import org.gradle.api.GradleException
2225
import org.gradle.api.Project
2326
import org.gradle.api.file.Directory
@@ -32,6 +35,7 @@ import org.gradle.kotlin.dsl.newInstance
3235
internal open class MyProjectProviders(
3336
projectBuildDirectory: DirectoryProperty,
3437
providerFactory: ProviderFactory,
38+
projectDirectoryHierarchy: List<Directory>,
3539
ext: DataConnectExtension
3640
) {
3741

@@ -42,6 +46,7 @@ internal open class MyProjectProviders(
4246
) : this(
4347
projectBuildDirectory = project.layout.buildDirectory,
4448
providerFactory = project.providers,
49+
projectDirectoryHierarchy = project.projectDirectoryHierarchy(),
4550
ext = project.extensions.getByType<DataConnectExtension>()
4651
)
4752

@@ -51,10 +56,54 @@ internal open class MyProjectProviders(
5156
providerFactory.provider {
5257
ext.firebaseToolsVersion
5358
?: throw GradleException(
54-
"dataconnect.firebaseToolsVersion must be set in your build.gradle or build.gradle.kts " +
59+
"dataconnect.firebaseToolsVersion must be set in your " +
60+
"build.gradle or build.gradle.kts " +
5561
"(error code xbmvkc3mtr)"
5662
)
5763
}
64+
65+
val localConfigFiles: Provider<List<RegularFile>> = providerFactory.provider {
66+
projectDirectoryHierarchy.map { it.file("dataconnect.local.toml") }
67+
}
68+
69+
val localConfigs: Provider<List<LocalConfig>> = run {
70+
val lazyResult = lazy(LazyThreadSafetyMode.PUBLICATION) {
71+
projectDirectoryHierarchy
72+
.map { it.file("dataconnect.local.toml").asFile }
73+
.mapNotNull { file ->
74+
val text = file.runCatching { readText() }.fold(
75+
onSuccess = { it },
76+
onFailure = { exception ->
77+
if (exception is FileNotFoundException) {
78+
null // ignore non-existent config files
79+
} else {
80+
throw GradleException(
81+
"reading file failed: ${file.absolutePath} ($exception)" +
82+
" (error code bj7dxvvw5p)",
83+
exception
84+
)
85+
}
86+
}
87+
)
88+
if (text === null) null else Pair(file, text)
89+
}.map { (file, text) ->
90+
val toml = Toml { this.ignoreUnknownKeys = true }
91+
toml.runCatching {
92+
decodeFromString<LocalConfig>(text, "dataconnect").copy(srcFile = file)
93+
}.fold(
94+
onSuccess = { it },
95+
onFailure = { exception ->
96+
throw GradleException(
97+
"parsing toml file failed: ${file.absolutePath} ($exception)" +
98+
" (error code 44dkc2vvpq)",
99+
exception
100+
)
101+
}
102+
)
103+
}
104+
}
105+
providerFactory.provider { lazyResult.value }
106+
}
58107
}
59108

60109
internal open class MyVariantProviders(
@@ -105,3 +154,11 @@ private val Project.firebaseToolsSetupTask: FirebaseToolsSetupTask
105154
}
106155
return tasks.single()
107156
}
157+
158+
private fun Project.projectDirectoryHierarchy(): List<Directory> = buildList {
159+
var curProject: Project? = this@projectDirectoryHierarchy
160+
while (curProject !== null) {
161+
add(curProject.layout.projectDirectory)
162+
curProject = curProject.parent
163+
}
164+
}

dataconnect/dataconnect.local.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[dataconnect]
2+
3+
# The path of the "npm" executable to use to install firebase-tools.
4+
# Setting this is normally not necessary; however, if "npm" is not in the global
5+
# PATH, or the wrong version is in the global PATH, then setting this to the absolute
6+
# path of the npm executable to use works around that problem. Setting it to null
7+
# uses "npm" as found in the PATH.
8+
#
9+
# eg. npmExecutable = "/home/myusername/local/nvm/versions/node/v20.13.1/bin/npm"
10+
npmExecutable = null

0 commit comments

Comments
 (0)