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

Add persistent cache for toolchain auto-detection mechanism #28966

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2024 the original author or authors.
*
* 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 org.gradle.cache.internal.scopes

import org.gradle.cache.internal.VersionStrategy
import org.gradle.test.fixtures.file.TestNameTestDirectoryProvider
import org.junit.Rule
import spock.lang.Specification

class NamedCacheScopeMappingTest extends Specification {
@Rule
TestNameTestDirectoryProvider tmpDir = new TestNameTestDirectoryProvider(getClass())
def userHomeCaches = tmpDir.createDir("caches")
def rootDir = tmpDir.createDir("root")
def mapping = new NamedCacheScopeMapping(userHomeCaches, "version")

def "null base dir maps to user home directory"() {
expect:
mapping.getBaseDirectory(null, "key", VersionStrategy.CachePerVersion) == userHomeCaches.file("version/key")
mapping.getBaseDirectory(null, "key", VersionStrategy.SharedCache) == userHomeCaches.file("key")
}

def "uses specified base dir"() {
expect:
mapping.getBaseDirectory(rootDir, "key", VersionStrategy.CachePerVersion) == rootDir.file("version/key")
mapping.getBaseDirectory(rootDir, "key", VersionStrategy.SharedCache) == rootDir.file("key")
}

def "can't use badly-formed key '#key'"() {
when:
mapping.getBaseDirectory(null, key, VersionStrategy.CachePerVersion)

then:
thrown(IllegalArgumentException)

where:
key << ["1.11", "1.2.3.4", "", "/", "..", "c:/some-dir", "\n", "a\\b", " no white space "]
}

def "can use well-formed key '#key'"() {
when:
mapping.getBaseDirectory(null, key, VersionStrategy.CachePerVersion)

then:
noExceptionThrown()

where:
key << ["abc", "a/b/c", "module-1.2"]
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 the original author or authors.
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,24 +19,23 @@
import com.google.common.annotations.VisibleForTesting;
import org.gradle.cache.internal.CacheScopeMapping;
import org.gradle.cache.internal.VersionStrategy;
import org.gradle.util.GradleVersion;

import javax.annotation.Nullable;
import java.io.File;
import java.util.regex.Pattern;

public class DefaultCacheScopeMapping implements CacheScopeMapping {
public class NamedCacheScopeMapping implements CacheScopeMapping {

@VisibleForTesting
public static final String GLOBAL_CACHE_DIR_NAME = "caches";
private static final Pattern CACHE_KEY_NAME_PATTERN = Pattern.compile("\\p{Alpha}+[-/.\\w]*");

private final File globalCacheDir;
private final GradleVersion version;
private final String name;

public DefaultCacheScopeMapping(File rootDir, GradleVersion version) {
public NamedCacheScopeMapping(File rootDir, String name) {
this.globalCacheDir = rootDir;
this.version = version;
this.name = name;
}

@Override
Expand All @@ -59,7 +58,7 @@ private File getRootDirectory(@Nullable File scope) {
private File getCacheDir(File rootDir, VersionStrategy versionStrategy, String subDir) {
switch (versionStrategy) {
case CachePerVersion:
return new File(rootDir, version.getVersion() + "/" + subDir);
return new File(rootDir, name + "/" + subDir);
case SharedCache:
return new File(rootDir, subDir);
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,58 @@ class DaemonToolchainIntegrationTest extends AbstractIntegrationSpec implements
fails("help")
failure.assertHasDescription("Cannot find a Java installation on your machine")
}

@Requires(IntegTestPreconditions.JavaHomeWithDifferentVersionAvailable)
def "Given non existing toolchain metadata cache When execute any consecutive tasks Then metadata is resolved only for the first build"() {
def otherJvm = AvailableJavaHomes.differentVersion

given:
cleanToolchainsMetadataCache()
writeJvmCriteria(otherJvm)

when:
def results = (0..2).collect {
withInstallations(otherJvm).executer
.withArgument("--info")
.withTasks("help")
.run()
}

then:
results.size() == 3
1 == countReceivedJvmInstallationsMetadata(otherJvm, results[0].plainTextOutput)
0 == countReceivedJvmInstallationsMetadata(otherJvm, results[1].plainTextOutput)
0 == countReceivedJvmInstallationsMetadata(otherJvm, results[2].plainTextOutput)
}

@Requires(IntegTestPreconditions.JavaHomeWithDifferentVersionAvailable)
def "Given daemon toolchain and task with specific toolchain When execute task Then metadata is resolved only one time storing resolution into cache shared between daemon and task toolchain"() {
def otherJvm = AvailableJavaHomes.differentVersion
def otherMetadata = AvailableJavaHomes.getJvmInstallationMetadata(otherJvm)

given:
cleanToolchainsMetadataCache()
writeJvmCriteria(otherJvm)
buildFile << """
apply plugin: 'jvm-toolchains'
tasks.register('exec', JavaExec) {
javaLauncher.set(javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of($otherMetadata.languageVersion.majorVersion)
vendor = JvmVendorSpec.matching("${otherMetadata.vendor.knownVendor.name()}")
})
mainClass.set("None")
jvmArgs = ['-version']
}
"""

when:
def result = withInstallations(otherJvm).executer
.withToolchainDetectionEnabled()
.withArgument("--info")
.withTasks("exec")
.run()

then:
1 == countReceivedJvmInstallationsMetadata(otherJvm, result.plainTextOutput)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ public BuildActionsFactory(ServiceRegistry loggingServices) {
this.basicServices = ServiceRegistryBuilder.builder()
.scopeStrictly(Scope.Global.class)
.displayName("Basic global services")
.parent(loggingServices)
.parent(NativeServices.getInstance())
.provider(new BasicGlobalScopeServices())
.build();
Expand Down Expand Up @@ -248,6 +247,7 @@ private Runnable runBuildInSingleUseDaemon(StartParameterInternal startParameter
private ServiceRegistry createGlobalClientServices() {
ServiceRegistryBuilder builder = ServiceRegistryBuilder.builder()
.displayName("Daemon client global services")
.parent(loggingServices)
.parent(NativeServices.getInstance());
builder.parent(basicServices);
return builder.provider(new DaemonClientGlobalServices()).build();
Expand Down
3 changes: 3 additions & 0 deletions platforms/jvm/jvm-services/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies {
api(project(":enterprise-logging"))
api(project(":file-temp"))
api(project(":file-collections"))
api(project(":persistent-cache"))
api(project(":process-services"))

api(libs.inject)
Expand All @@ -44,6 +45,8 @@ dependencies {

implementation(project(":functional"))
implementation(project(":native"))
implementation(project(":logging"))
implementation(project(":serialization"))

implementation(libs.guava)
implementation(libs.asm)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright 2024 the original author or authors.
*
* 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 org.gradle.internal.jvm.inspection

import org.gradle.api.internal.file.TestFiles
import org.gradle.cache.internal.DefaultCacheFactory
import org.gradle.cache.internal.DefaultFileLockManagerTestHelper
import org.gradle.cache.internal.VersionStrategy
import org.gradle.cache.internal.scopes.NamedCacheScopeMapping
import org.gradle.initialization.GradleUserHomeDirProvider
import org.gradle.initialization.layout.GlobalCacheDir
import org.gradle.integtests.fixtures.AbstractIntegrationSpec
import org.gradle.integtests.fixtures.AvailableJavaHomes
import org.gradle.internal.concurrent.DefaultExecutorFactory
import org.gradle.internal.jvm.Jvm
import org.gradle.internal.operations.BuildOperationContext
import org.gradle.internal.operations.BuildOperationRunner
import org.gradle.internal.operations.RunnableBuildOperation
import org.gradle.jvm.toolchain.internal.InstallationLocation
import org.gradle.process.internal.DefaultExecHandleBuilder
import org.gradle.test.precondition.Requires
import org.gradle.test.preconditions.IntegTestPreconditions
import org.gradle.util.GradleVersion
import org.gradle.util.internal.Resources
import org.junit.Rule

import java.util.concurrent.Executors

import static org.junit.Assert.assertEquals

class CachingJvmMetadataDetectorIntegrationTest extends AbstractIntegrationSpec {

@Rule
public final Resources resources = new Resources(testDirectoryProvider)

def "Given invalid toolchain installation When obtaining metadata Then failure metadata is returned"() {
given:
def invalidToolchainLocation = new File("test")
def expectedMetadata = JvmInstallationMetadata.failure(invalidToolchainLocation, "No such directory: test")

when:
def cacheDetector = createCachingJvmMetadataDetector()
def metadata = cacheDetector.getMetadata(createLocation(invalidToolchainLocation))

then:
assertFailureJvmMetadata(expectedMetadata, metadata)
}

@Requires(IntegTestPreconditions.JavaHomeWithDifferentVersionAvailable)
def "Given valid toolchain installations When obtaining metadata Then expected valid metadata is returned"() {
given:
def currentMetadata = AvailableJavaHomes.getJvmInstallationMetadata(Jvm.current())
def otherMetadata = AvailableJavaHomes.getJvmInstallationMetadata(AvailableJavaHomes.differentJdk)

when:
def cacheDetector = createCachingJvmMetadataDetector()
def currentMetadataResult = cacheDetector.getMetadata(createLocation(currentMetadata.javaHome.toFile()))
def otherMetadataResult = cacheDetector.getMetadata(createLocation(otherMetadata.javaHome.toFile()))

then:
assertValidJvmMetadata(currentMetadata, currentMetadataResult)
assertValidJvmMetadata(otherMetadata, otherMetadataResult)
}

def "Given valid existing cache When deserialize entry with known path Then expected valid metadata is returned"() {
def expectedJavaHome = new File("test/toolchain/path")
def expectedMetadata = JvmInstallationMetadata.from(expectedJavaHome, "11.0.16.1", "Amazon.com Inc.", "OpenJDK Runtime Environment",
"11.0.16.1+9-LTS", "OpenJDK 64-Bit Server VM", "11.0.16.1+9-LTS", "Amazon.com Inc.", "x86_64")
def expectedInstallationLocation = Mock(InstallationLocation) {
getCanonicalFile() >> expectedJavaHome
}

given:
restoreToolchainCacheFromResources("valid-toolchains-cache.bin")

when:
def cacheDetector = createCachingJvmMetadataDetector()
def metadata = cacheDetector.getMetadata(expectedInstallationLocation)

then:
assertValidJvmMetadata(expectedMetadata, metadata)
}

def "Given corrupted cache When deserialize entry from invalid path Then failure metadata is returned"() {
def expectedJavaHome = new File("test/toolchain/path")
def expectedMetadata = JvmInstallationMetadata.failure(null, "No such directory: null")
def expectedInstallationLocation = Mock(InstallationLocation) {
getCanonicalFile() >> expectedJavaHome
}

given:
restoreToolchainCacheFromResources("corrupted-toolchains-cache.bin")

when:
def cacheDetector = createCachingJvmMetadataDetector()
def metadata = cacheDetector.getMetadata(expectedInstallationLocation)

then:
assertFailureJvmMetadata(expectedMetadata, metadata)
}

def "Given corrupted cache When deserialize entry from valid path Then expected valid metadata is returned from installation"() {
def currentMetadata = AvailableJavaHomes.getJvmInstallationMetadata(Jvm.current())
def expectedJavaHome = new File("test/toolchain/path")
def expectedInstallationLocation = Mock(InstallationLocation) {
getCanonicalFile() >> expectedJavaHome
getLocation() >> currentMetadata.javaHome.toFile()
}

given:
restoreToolchainCacheFromResources("corrupted-toolchains-cache.bin")

when:
def cacheDetector = createCachingJvmMetadataDetector()
def metadata = cacheDetector.getMetadata(expectedInstallationLocation)

then:
assertValidJvmMetadata(currentMetadata, metadata)
}

private InstallationLocation createLocation(File file) {
return InstallationLocation.autoDetected(file, "test")
}

private CachingJvmMetadataDetector createCachingJvmMetadataDetector() {
def delegate = new DefaultJvmMetadataDetector(
() -> new DefaultExecHandleBuilder(TestFiles.pathToFileResolver(), Executors.newCachedThreadPool()),
TestFiles.tmpDirTemporaryFileProvider(temporaryFolder.getTestDirectory())
)
def globalCacheDir = new GlobalCacheDir(createHomeDirProvider())
def cacheFactory = new DefaultCacheFactory(DefaultFileLockManagerTestHelper.createDefaultFileLockManager(), new DefaultExecutorFactory(), createBuildOperationRunner())
def jvmMetadataCacheBuildFactory = new JvmInstallationMetadataCacheBuildFactory(cacheFactory, globalCacheDir.dir)
return new CachingJvmMetadataDetector(delegate, jvmMetadataCacheBuildFactory)
}

private void restoreToolchainCacheFromResources(String resourceCache) {
def globalCacheDir = new GlobalCacheDir(createHomeDirProvider())
def scopeMapping = new NamedCacheScopeMapping(globalCacheDir.dir, GradleVersion.current().version)
def cacheMetadataDir = scopeMapping.getBaseDirectory(globalCacheDir.dir, "toolchainsMetadata", VersionStrategy.CachePerVersion)
def targetCacheMetadataFile = new File(cacheMetadataDir, "toolchainsCache.bin")
resources.getResource(resourceCache).copyTo(targetCacheMetadataFile)
}

private GradleUserHomeDirProvider createHomeDirProvider() {
new GradleUserHomeDirProvider() {
@Override
File getGradleUserHomeDirectory() {
return temporaryFolder.getTestDirectory()
}
}
}

private BuildOperationRunner createBuildOperationRunner() {
Stub(BuildOperationRunner) {
run(_ as RunnableBuildOperation) >> { RunnableBuildOperation operation ->
def context = Stub(BuildOperationContext)
operation.run(context)
}
}
}

private void assertFailureJvmMetadata(JvmInstallationMetadata expected, JvmInstallationMetadata actual) {
assertEquals(expected.isValidInstallation(), actual.isValidInstallation())
assertEquals(expected.errorMessage, actual.errorMessage)
}

private void assertValidJvmMetadata(JvmInstallationMetadata expected, JvmInstallationMetadata actual) {
assertEquals(expected.javaHome, actual.javaHome)
assertEquals(expected.languageVersion, actual.languageVersion)
assertEquals(expected.vendor?.knownVendor, actual.vendor?.knownVendor)
assertEquals(expected.toString(), actual.toString())
}
}