From ab3b3004685e87f36f93c551049a1bd14f7731e8 Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Fri, 7 Aug 2026 21:02:14 +0100 Subject: [PATCH 1/2] fix(clang): resolve libc mode and CUDA support Resolve AUTO once after the clang-dumper executable is selected and carry the concrete mode through resource preparation, cache state, and per-translation-unit configuration. Keep local and plugin paths on SYSTEM, including the system Clang resource directory for built-in CUDA. Make CUDA support detection return false only for a validated manifest with no compatible host platform; propagate download, cache, parse, validation, and component failures. Tests: CudaResourcesTest (16), ClangResourcesTest (27), CxxCudaTest (6) --- .../pt/up/fe/specs/clang/ClangAstKeys.java | 5 -- .../src/pt/up/fe/specs/clang/ClangFiles.java | 10 ++- .../pt/up/fe/specs/clang/ClangResources.java | 43 +++++++++---- .../pt/up/fe/specs/clang/CudaResources.java | 61 ++++++++++++------- .../clang/codeparser/ParallelCodeParser.java | 13 ++-- .../fe/specs/clang/dumper/ClangAstDumper.java | 5 +- .../up/fe/specs/clang/ClangResourcesTest.java | 43 ++++++++++--- .../up/fe/specs/clang/CudaResourcesTest.java | 39 ++++++++++++ 8 files changed, 158 insertions(+), 61 deletions(-) diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstKeys.java b/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstKeys.java index 1fef8afdf..ec17d8a8d 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstKeys.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstKeys.java @@ -16,7 +16,6 @@ import org.suikasoft.jOptions.Datakey.DataKey; import org.suikasoft.jOptions.Datakey.KeyFactory; import org.suikasoft.jOptions.Interfaces.DataStore; -import pt.up.fe.specs.clang.ClangAstWebResource.LocalBuild; import pt.up.fe.specs.clava.ClavaLog; import pt.up.fe.specs.clava.ClavaOptions; import pt.up.fe.specs.clava.language.Standard; @@ -99,10 +98,6 @@ static DataStore toDataStore(List flags) { config.add(ClavaOptions.FLAGS_LIST, parsedFlags); - if (ClangAstWebResource.getDumperSource() instanceof LocalBuild) { - config.set(ClangAstKeys.LIBC_CXX_MODE, LibcMode.SYSTEM); - } - return config; } diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/ClangFiles.java b/ClangAstParser/src/pt/up/fe/specs/clang/ClangFiles.java index 1fcbf321b..d250a9ab8 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/ClangFiles.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/ClangFiles.java @@ -15,7 +15,15 @@ import java.io.File; import java.util.List; +import java.util.Objects; -public record ClangFiles(File clangExecutable, List builtinIncludes, File systemResourceDir) { +public record ClangFiles(File clangExecutable, List builtinIncludes, File systemResourceDir, + LibcMode libcMode) { + public ClangFiles { + Objects.requireNonNull(libcMode, "libcMode"); + if (libcMode == LibcMode.AUTO) { + throw new IllegalArgumentException("Clang files must use a concrete libc mode"); + } + } } diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/ClangResources.java b/ClangAstParser/src/pt/up/fe/specs/clang/ClangResources.java index c7fcffe77..041f66e56 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/ClangResources.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/ClangResources.java @@ -36,6 +36,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -64,22 +65,26 @@ public File getBuiltinCudaLib() { return CudaResources.getBuiltinCudaLib(options.get(CodeParser.DUMPER_FOLDER).toPath()); } - public ClangFiles getClangFiles(LibcMode libcMode) { + public ClangFiles getClangFiles(LibcMode requestedLibcMode) { var source = ClangAstWebResource.getDumperSource(); var useBuiltinCuda = options.get(CodeParser.CUDA_PATH).equalsIgnoreCase(CodeParser.getBuiltinOption()); + var forceSystemLibc = source instanceof LocalBuild || ClangAstDumper.usePlugin(); if (source instanceof LocalBuild localBuild) { var clangExecutable = getLocalExecutable(localBuild.folder()); + var libcMode = resolveLibcMode(clangExecutable, requestedLibcMode, forceSystemLibc); var systemResourceDir = libcMode == LibcMode.SYSTEM && useBuiltinCuda ? findSystemClangResourceDir(null) : null; - return new ClangFiles(clangExecutable, List.of(), systemResourceDir); + return new ClangFiles(clangExecutable, List.of(), systemResourceDir, libcMode); } var resourceFolder = getClangResourceFolder(); - var key = libcMode.name() + "_" + useBuiltinCuda + "_" + source + "_" - + resourceFolder.getAbsolutePath(); + var manifest = ClangAstWebResource.getManifest(resourceFolder); + File clangExecutable = prepareResources(manifest, resourceFolder); + var libcMode = resolveLibcMode(clangExecutable, requestedLibcMode, forceSystemLibc); + var key = libcMode.name() + "_" + useBuiltinCuda + "_" + source + "_" + resourceFolder.getAbsolutePath(); var cached = CLANG_FILES_CACHE.get(key); if (isUsable(cached)) { SpecsLogs.debug(() -> "Using cached version of Clang files: " + cached.files()); @@ -90,9 +95,7 @@ public ClangFiles getClangFiles(LibcMode libcMode) { CLANG_FILES_CACHE.remove(key, cached); } - var manifest = ClangAstWebResource.getManifest(resourceFolder); - File clangExecutable = prepareResources(manifest, resourceFolder); - var includes = prepareIncludes(manifest, clangExecutable, libcMode); + var includes = prepareIncludes(manifest, libcMode); var systemResourceDir = libcMode == LibcMode.SYSTEM && useBuiltinCuda ? prepareSystemClangResourceDir(manifest) : null; @@ -104,7 +107,8 @@ public ClangFiles getClangFiles(LibcMode libcMode) { touchUse(resourceFolder, includes.extractedFolder()); updateLastUsedAndCleanupStaleVersions(resourceFolder, includes.extractedFolder()); - var newFiles = new CachedClangFiles(new ClangFiles(clangExecutable, includes.folders(), systemResourceDir), + var newFiles = new CachedClangFiles(new ClangFiles(clangExecutable, includes.folders(), systemResourceDir, + libcMode), includes.extractedFolder()); var existingFiles = CLANG_FILES_CACHE.putIfAbsent(key, newFiles); var selectedFiles = existingFiles == null ? newFiles : existingFiles; @@ -113,6 +117,22 @@ public ClangFiles getClangFiles(LibcMode libcMode) { return selectedFiles.files(); } + static LibcMode resolveLibcMode(File clangExecutable, LibcMode requestedLibcMode, boolean forceSystem) { + Objects.requireNonNull(clangExecutable, "clangExecutable"); + Objects.requireNonNull(requestedLibcMode, "requestedLibcMode"); + + if (forceSystem) { + return LibcMode.SYSTEM; + } + + return switch (requestedLibcMode) { + case AUTO -> useBuiltinLibc(clangExecutable, requestedLibcMode) + ? LibcMode.BUILTIN_AND_LIBC + : LibcMode.SYSTEM; + case BUILTIN_AND_LIBC, SYSTEM -> requestedLibcMode; + }; + } + private boolean isUsable(CachedClangFiles cached) { if (cached == null) { return false; @@ -301,11 +321,8 @@ private static ProcessOutputAsString runClangAstDumper(File clangExecutable, Fil return SpecsSystem.runProcess(arguments, true, false); } - private PreparedIncludes prepareIncludes(ClangDumperManifest manifest, File clangExecutable, - LibcMode libcMode) { - var useBuiltinLibc = useBuiltinLibc(clangExecutable, libcMode); - - if (!useBuiltinLibc) { + private PreparedIncludes prepareIncludes(ClangDumperManifest manifest, LibcMode libcMode) { + if (libcMode == LibcMode.SYSTEM) { return new PreparedIncludes(List.of(), null); } diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/CudaResources.java b/ClangAstParser/src/pt/up/fe/specs/clang/CudaResources.java index 4cf8718d9..93afe53dd 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/CudaResources.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/CudaResources.java @@ -47,6 +47,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.regex.Pattern; @@ -140,7 +141,10 @@ static CudaPlatform requireSupportedPlatform() { } static CudaPlatform requireSupportedPlatform(NvidiaCudaManifest manifest) { - return getCurrentPlatform(manifest); + var platform = SupportedPlatform.getCurrentPlatform(); + var architecture = System.getProperty("os.arch"); + return findSupportedPlatform(manifest) + .orElseThrow(() -> unsupportedPlatform(manifest, platform, architecture)); } static boolean isSupportedPlatform() { @@ -148,12 +152,7 @@ static boolean isSupportedPlatform() { } static boolean isSupportedPlatform(Path cacheRoot) { - try { - getCurrentPlatform(cacheRoot); - return true; - } catch (RuntimeException e) { - return false; - } + return findSupportedPlatform(getCurrentManifest(cacheRoot)).isPresent(); } static CudaPlatform getCurrentPlatform() { @@ -161,18 +160,25 @@ static CudaPlatform getCurrentPlatform() { } static CudaPlatform getCurrentPlatform(Path cacheRoot) { - var releaseTag = ClangAstWebResource.getCudaReleaseTag(); - var releaseFolder = getReleaseFolder(cacheRoot, releaseTag); - claimReleaseInUse(cacheRoot, releaseFolder); - return getCurrentPlatform(getManifest(cacheRoot, releaseFolder)); + return requireSupportedPlatform(getCurrentManifest(cacheRoot)); } static CudaPlatform getCurrentPlatform(NvidiaCudaManifest manifest) { - return new CudaPlatform(getManifestPlatform(manifest, SupportedPlatform.getCurrentPlatform(), - System.getProperty("os.arch"))); + return requireSupportedPlatform(manifest); } static String getManifestPlatform(NvidiaCudaManifest manifest, SupportedPlatform platform, String architecture) { + return findManifestPlatform(manifest, platform, architecture) + .orElseThrow(() -> unsupportedPlatform(manifest, platform, architecture)); + } + + private static Optional findSupportedPlatform(NvidiaCudaManifest manifest) { + return findManifestPlatform(manifest, SupportedPlatform.getCurrentPlatform(), + System.getProperty("os.arch")).map(CudaPlatform::new); + } + + private static Optional findManifestPlatform(NvidiaCudaManifest manifest, SupportedPlatform platform, + String architecture) { Objects.requireNonNull(manifest, "manifest"); Objects.requireNonNull(platform, "platform"); Objects.requireNonNull(architecture, "architecture"); @@ -195,19 +201,28 @@ static String getManifestPlatform(NvidiaCudaManifest manifest, SupportedPlatform } } - var selectedPlatform = commonPlatforms.stream() + if (!missingComponents.isEmpty()) { + throw new RuntimeException("NVIDIA CUDA manifest is missing required components " + missingComponents + + ". Available manifest platform keys: " + getAvailablePlatformKeys(manifest)); + } + + return commonPlatforms.stream() .filter(candidate -> isCompatiblePlatform(candidate, platform, architecture)) .findFirst(); - if (missingComponents.isEmpty() && selectedPlatform.isPresent()) { - return selectedPlatform.get(); - } + } - var reason = missingComponents.isEmpty() - ? "no platform key is present in all required components and is compatible with this host" - : "the manifest is missing required components " + missingComponents; - throw new RuntimeException("Built-in CUDA is unsupported for host '" + platform + " (" + architecture - + ")': " + reason + ". Available manifest platform keys: " - + getAvailablePlatformKeys(manifest)); + private static RuntimeException unsupportedPlatform(NvidiaCudaManifest manifest, SupportedPlatform platform, + String architecture) { + return new RuntimeException("Built-in CUDA is unsupported for host '" + platform + " (" + architecture + + ")': no platform key is present in all required components and is compatible with this host" + + ". Available manifest platform keys: " + getAvailablePlatformKeys(manifest)); + } + + private static NvidiaCudaManifest getCurrentManifest(Path cacheRoot) { + var releaseTag = ClangAstWebResource.getCudaReleaseTag(); + var releaseFolder = getReleaseFolder(cacheRoot, releaseTag); + claimReleaseInUse(cacheRoot, releaseFolder); + return getManifest(cacheRoot, releaseFolder); } private static boolean isCompatiblePlatform(String manifestPlatform, SupportedPlatform hostPlatform, diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/ParallelCodeParser.java b/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/ParallelCodeParser.java index 9313859bd..91b7ca5b9 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/ParallelCodeParser.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/ParallelCodeParser.java @@ -18,7 +18,6 @@ import org.suikasoft.jOptions.Interfaces.DataStore; import pt.up.fe.specs.clang.ClangAstKeys; import pt.up.fe.specs.clang.ClangResources; -import pt.up.fe.specs.clang.LibcMode; import pt.up.fe.specs.clang.dumper.ClangAstData; import pt.up.fe.specs.clang.dumper.ClangAstDumper; import pt.up.fe.specs.clang.dumper.ClangAstParser; @@ -89,7 +88,6 @@ public App parse(List inputSources, List compilerOptions, ClavaCon ConcurrentLinkedQueue clangDump = new ConcurrentLinkedQueue<>(); DataStore options = ClangAstKeys.toDataStore(compilerOptions); - options.set(ClangAstKeys.LIBC_CXX_MODE, get(ClangAstKeys.LIBC_CXX_MODE)); // Add context to config // ClavaContext context = new ClavaContext(); @@ -110,13 +108,8 @@ public App parse(List inputSources, List compilerOptions, ClavaCon // ClangResources clangResources = new ClangResources(get(SHOW_CLANG_DUMP)); ClangResources clangResources = new ClangResources(this); - - if (ClangAstDumper.usePlugin()) { - set(ClangAstKeys.LIBC_CXX_MODE, LibcMode.SYSTEM); - ClavaLog.debug(() -> "In Linux, ClangAstDumper is a plugin. LIBC_CXX_MODE is reset to SYSTEM."); - } - var clangFiles = clangResources.getClangFiles(get(ClangAstKeys.LIBC_CXX_MODE)); + options.set(ClangAstKeys.LIBC_CXX_MODE, clangFiles.libcMode()); // File clangExecutable = clangResources.prepareResources(version); // List builtinIncludes = clangResources.prepareIncludes(clangExecutable, // get(ClangAstKeys.USE_PLATFORM_INCLUDES)); @@ -246,7 +239,9 @@ public App parse(List inputSources, List compilerOptions, ClavaCon app.getContext().pushApp(app); app.setSourcesFromStrings(allSources); - app.addConfig(ClangAstKeys.toDataStore(compilerOptions)); + DataStore appConfig = ClangAstKeys.toDataStore(compilerOptions); + appConfig.set(ClangAstKeys.LIBC_CXX_MODE, clangFiles.libcMode()); + app.addConfig(appConfig); // Applies several passes to make the tree resemble more the original code, e.g., remove implicit nodes from // original clang tree diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstDumper.java b/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstDumper.java index 8b56c1c29..b0c1463f0 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstDumper.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstDumper.java @@ -18,6 +18,7 @@ import org.suikasoft.jOptions.streamparser.LineStreamParser; import pt.up.fe.specs.clang.ClangAstKeys; import pt.up.fe.specs.clang.ClangResources; +import pt.up.fe.specs.clang.LibcMode; import pt.up.fe.specs.clang.cilk.CilkParser; import pt.up.fe.specs.clang.codeparser.CodeParser; import pt.up.fe.specs.clang.codeparser.ParallelCodeParser; @@ -260,8 +261,8 @@ else if (SourceType.isHeader(sourceFile)) { arguments.add("-resource-dir=" + systemResourceDir.getAbsolutePath()); } - // If it was determined that built-in includes will be used, disable system includes - if (ClangResources.useBuiltinLibc(clangExecutable, config.get(ClangAstKeys.LIBC_CXX_MODE))) { + // The parser has already resolved the libc policy before creating this per-file configuration. + if (config.get(ClangAstKeys.LIBC_CXX_MODE) == LibcMode.BUILTIN_AND_LIBC) { arguments.add("-nostdinc"); arguments.add("-nostdinc++"); } diff --git a/ClangAstParser/test/pt/up/fe/specs/clang/ClangResourcesTest.java b/ClangAstParser/test/pt/up/fe/specs/clang/ClangResourcesTest.java index bdd8f2379..3169d7b77 100644 --- a/ClangAstParser/test/pt/up/fe/specs/clang/ClangResourcesTest.java +++ b/ClangAstParser/test/pt/up/fe/specs/clang/ClangResourcesTest.java @@ -526,13 +526,18 @@ public void sameJvmInstancesReuseReleaseFilesAndPrepareIncludesOnlyForBuiltinLib } @Test - public void builtinCudaUsesSystemClangResourceWithSystemLibc() { + public void builtinCudaAutoSystemLibcUsesSameResourcePathAsExplicitSystem() { var parser = newParser(CodeParser.getBuiltinOption()); - var clangFiles = new ClangResources(parser).getClangFiles(LibcMode.SYSTEM); - - assertTrue(clangFiles.builtinIncludes().isEmpty()); - assertNotNull(clangFiles.systemResourceDir()); - assertTrue(clangFiles.systemResourceDir().isDirectory()); + var resources = new ClangResources(parser); + var autoFiles = resources.getClangFiles(LibcMode.AUTO); + var systemFiles = resources.getClangFiles(LibcMode.SYSTEM); + + assertEquals(LibcMode.SYSTEM, autoFiles.libcMode()); + assertEquals(LibcMode.SYSTEM, systemFiles.libcMode()); + assertTrue(autoFiles.builtinIncludes().isEmpty()); + assertEquals(systemFiles.systemResourceDir(), autoFiles.systemResourceDir()); + assertNotNull(autoFiles.systemResourceDir()); + assertTrue(autoFiles.systemResourceDir().isDirectory()); assertFalse(Files.exists(clangCacheRoot().resolve("includes"))); } @@ -562,8 +567,30 @@ public void libcDetectionIsScopedToTheExecutable() throws IOException { Files.writeString(builtinLibcDumper, "#!/bin/sh\nexit 1\n"); assertTrue(builtinLibcDumper.toFile().setExecutable(true)); - assertFalse(ClangResources.useBuiltinLibc(systemLibcDumper.toFile(), LibcMode.AUTO)); - assertTrue(ClangResources.useBuiltinLibc(builtinLibcDumper.toFile(), LibcMode.AUTO)); + assertEquals(LibcMode.SYSTEM, + ClangResources.resolveLibcMode(systemLibcDumper.toFile(), LibcMode.AUTO, false)); + assertEquals(LibcMode.BUILTIN_AND_LIBC, + ClangResources.resolveLibcMode(builtinLibcDumper.toFile(), LibcMode.AUTO, false)); + assertEquals(LibcMode.SYSTEM, + ClangResources.resolveLibcMode(systemLibcDumper.toFile(), LibcMode.SYSTEM, false)); + assertEquals(LibcMode.BUILTIN_AND_LIBC, + ClangResources.resolveLibcMode(builtinLibcDumper.toFile(), LibcMode.BUILTIN_AND_LIBC, false)); + } + + @Test + public void forcedBuildAndPluginModesResolveToSystemWithoutAutoState() throws IOException { + assumeTrue(!SupportedPlatform.getCurrentPlatform().isWindows(), "Shell fixtures require a Unix executable"); + + var dumper = tempFolder.resolve("dumper"); + Files.writeString(dumper, "#!/bin/sh\nexit 1\n"); + assertTrue(dumper.toFile().setExecutable(true)); + + assertEquals(LibcMode.SYSTEM, + ClangResources.resolveLibcMode(dumper.toFile(), LibcMode.BUILTIN_AND_LIBC, true)); + assertEquals(LibcMode.SYSTEM, + ClangResources.resolveLibcMode(dumper.toFile(), LibcMode.AUTO, true)); + assertThrows(IllegalArgumentException.class, + () -> new ClangFiles(dumper.toFile(), List.of(), null, LibcMode.AUTO)); } private CodeParser newParser(String cudaPath) { diff --git a/ClangAstParser/test/pt/up/fe/specs/clang/CudaResourcesTest.java b/ClangAstParser/test/pt/up/fe/specs/clang/CudaResourcesTest.java index 3a2576bbe..09f27952d 100644 --- a/ClangAstParser/test/pt/up/fe/specs/clang/CudaResourcesTest.java +++ b/ClangAstParser/test/pt/up/fe/specs/clang/CudaResourcesTest.java @@ -135,6 +135,35 @@ public void manifestRejectsHostWhenARequiredComponentLacksThePlatform() { assertTrue(error.getMessage().contains("cuda_cccl")); } + @Test + public void supportDetectionReturnsFalseOnlyForAValidatedUnsupportedHost() throws IOException { + var unsupportedPlatform = SupportedPlatform.getCurrentPlatform().isWindows() + ? "linux-riscv64" + : "windows-x86_64"; + writeCachedManifest(manifestJson(unsupportedPlatform)); + + assertFalse(CudaResources.isSupportedPlatform(tempFolder)); + } + + @Test + public void supportDetectionPropagatesManifestAndCacheFailures() throws IOException { + writeCachedManifest("not-json"); + assertThrows(RuntimeException.class, () -> CudaResources.isSupportedPlatform(tempFolder)); + + var invalidManifestRoot = Files.createDirectory(tempFolder.resolve("invalid")).toAbsolutePath(); + writeCachedManifest(invalidManifestRoot, manifestJson() + .replace("\"release_product\": \"cuda\"", "\"release_product\": \"other\"")); + assertThrows(RuntimeException.class, () -> CudaResources.isSupportedPlatform(invalidManifestRoot)); + + var missingComponentRoot = Files.createDirectory(tempFolder.resolve("missing")).toAbsolutePath(); + writeCachedManifest(missingComponentRoot, manifestJson().replace("\"cuda_cccl\":", "\"missing\":")); + assertThrows(RuntimeException.class, () -> CudaResources.isSupportedPlatform(missingComponentRoot)); + + var cacheFile = tempFolder.resolve("cache-file"); + Files.writeString(cacheFile, "not-a-directory"); + assertThrows(RuntimeException.class, () -> CudaResources.isSupportedPlatform(cacheFile)); + } + @Test public void additionalCompatibleManifestPlatformNeedsNoJavaSupportWhitelist() { var additionalPlatform = "linux-riscv64"; @@ -520,6 +549,16 @@ private static String hostPlatform() { return PLATFORM; } + private void writeCachedManifest(String json) throws IOException { + writeCachedManifest(tempFolder, json); + } + + private void writeCachedManifest(Path cacheRoot, String json) throws IOException { + var releaseFolder = cacheRoot.resolve("cuda").resolve(ClangAstWebResource.getCudaReleaseTag()); + Files.createDirectories(releaseFolder); + Files.writeString(releaseFolder.resolve(CudaResources.getManifestFilename(RELEASE)), json); + } + private static void writeTarXz(Path archive, Map files) throws IOException { try (OutputStream output = Files.newOutputStream(archive); var xzOutput = new XZCompressorOutputStream(output); From 728559399bdfc22928df2cb42643b417f09d84e9 Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Sun, 9 Aug 2026 00:39:54 +0100 Subject: [PATCH 2/2] refactor(clang): publish CUDA resources at release root Keep only completed CUDA installations persistent and use CacheFiles staging for manifest and archive assembly. --- .../pt/up/fe/specs/clang/CudaResources.java | 280 +++++++-------- .../up/fe/specs/clang/ClangResourcesTest.java | 11 +- .../up/fe/specs/clang/CudaResourcesTest.java | 319 +++++++++--------- 3 files changed, 317 insertions(+), 293 deletions(-) diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/CudaResources.java b/ClangAstParser/src/pt/up/fe/specs/clang/CudaResources.java index 93afe53dd..28641b5ab 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/CudaResources.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/CudaResources.java @@ -56,8 +56,8 @@ * Downloads the NVIDIA redistribution packages required by Clang and assembles them into the CUDA root expected by * the bundled dumper. * - *

CUDA resources are release-addressed. Archives from different CUDA releases therefore never share a cache - * destination, even when NVIDIA publishes identical bytes for both releases.

+ *

CUDA resources are release-addressed. The published release directory is the complete CUDA installation; + * manifests and downloaded archives used to build it live in staging until that directory is published.

*/ final class CudaResources { @@ -66,8 +66,6 @@ final class CudaResources { static final String PLATFORM_FILENAME = ".platform"; private static final String CUDA_FOLDERNAME = "cuda"; - private static final String CUDA_LIB_FOLDERNAME = "cudalib"; - private static final String ARCHIVES_FOLDERNAME = "archives"; private static final String MANIFEST_FILENAME_PREFIX = "redistrib_"; private static final String MANIFEST_FILENAME_SUFFIX = ".json"; private static final Set MANIFEST_FIELDS = Set.of("release_date", "release_label", "release_product"); @@ -88,52 +86,14 @@ private CudaResources() { static File getBuiltinCudaLib(Path cacheRoot) { var releaseTag = ClangAstWebResource.getCudaReleaseTag(); var releaseFolder = getReleaseFolder(cacheRoot, releaseTag); - claimReleaseInUse(cacheRoot, releaseFolder); - var manifest = getManifest(cacheRoot, releaseFolder); - var platform = requireSupportedPlatform(manifest); - var platformFolder = getPlatformFolder(cacheRoot, releaseTag, platform); - var installationFolder = getInstallationFolder(platformFolder); // A published installation is immutable. A malformed one is an operator error, not an invitation to repair it // in place, because doing so could race with a reader that already selected this release. - if (Files.exists(installationFolder.toPath(), LinkOption.NOFOLLOW_LINKS)) { - return useExistingInstallation(cacheRoot, platformFolder, platform, installationFolder); + if (Files.exists(releaseFolder.toPath(), LinkOption.NOFOLLOW_LINKS)) { + return useExistingInstallation(cacheRoot, releaseFolder, releaseTag); } - claimInUse(cacheRoot, platformFolder); - return install(cacheRoot, platformFolder, releaseTag, platform, manifest, CudaResources::getArchiveResource); - } - - private static void claimReleaseInUse(Path cacheRoot, File releaseFolder) { - CacheFiles.withMaintenanceLock(cacheRoot, () -> { - try { - Files.createDirectories(releaseFolder.toPath()); - } catch (IOException e) { - throw new UncheckedIOException("Could not create CUDA release folder '" + releaseFolder + "'", e); - } - - CacheFiles.touch(releaseFolder.toPath()); - }); - } - - static void claimInUse(Path cacheRoot, File platformFolder) { - CacheFiles.withMaintenanceLock(cacheRoot, () -> { - var platformPath = platformFolder.toPath(); - var releasePath = platformPath.getParent(); - if (releasePath == null) { - throw new RuntimeException("CUDA platform folder is not below a release folder: '" - + platformFolder + "'"); - } - - try { - Files.createDirectories(platformPath); - } catch (IOException e) { - throw new UncheckedIOException("Could not create CUDA platform folder '" + platformPath + "'", e); - } - - CacheFiles.touch(releasePath); - CacheFiles.touch(platformPath); - }); + return install(cacheRoot, releaseTag, getManifestResource(releaseTag), CudaResources::getArchiveResource); } static CudaPlatform requireSupportedPlatform() { @@ -148,11 +108,16 @@ static CudaPlatform requireSupportedPlatform(NvidiaCudaManifest manifest) { } static boolean isSupportedPlatform() { - return isSupportedPlatform(ClangResources.getDefaultTempFolder().toPath()); + return isSupportedPlatform(ClangResources.getDefaultTempFolder().toPath(), + getManifestResource(ClangAstWebResource.getCudaReleaseTag())); } static boolean isSupportedPlatform(Path cacheRoot) { - return findSupportedPlatform(getCurrentManifest(cacheRoot)).isPresent(); + return isSupportedPlatform(cacheRoot, getManifestResource(ClangAstWebResource.getCudaReleaseTag())); + } + + static boolean isSupportedPlatform(Path cacheRoot, FileResourceProvider manifestResource) { + return findSupportedPlatform(getCurrentManifest(cacheRoot, manifestResource)).isPresent(); } static CudaPlatform getCurrentPlatform() { @@ -220,9 +185,28 @@ private static RuntimeException unsupportedPlatform(NvidiaCudaManifest manifest, private static NvidiaCudaManifest getCurrentManifest(Path cacheRoot) { var releaseTag = ClangAstWebResource.getCudaReleaseTag(); - var releaseFolder = getReleaseFolder(cacheRoot, releaseTag); - claimReleaseInUse(cacheRoot, releaseFolder); - return getManifest(cacheRoot, releaseFolder); + return getCurrentManifest(cacheRoot, releaseTag, getManifestResource(releaseTag)); + } + + private static NvidiaCudaManifest getCurrentManifest(Path cacheRoot, FileResourceProvider manifestResource) { + var releaseTag = ClangAstWebResource.getCudaReleaseTag(); + return getCurrentManifest(cacheRoot, releaseTag, manifestResource); + } + + private static NvidiaCudaManifest getCurrentManifest(Path cacheRoot, String releaseTag, + FileResourceProvider manifestResource) { + var cudaRoot = cacheRoot.resolve(CUDA_FOLDERNAME); + CacheFiles.deleteUnlockedStagingLocks(cacheRoot, cudaRoot); + var stagingDirectory = CacheFiles.createStagingDirectory(cacheRoot, cudaRoot, "." + releaseTag + ".tmp-"); + try { + return downloadManifest(cacheRoot, stagingDirectory.path(), releaseTag, manifestResource); + } finally { + try { + CacheFiles.delete(stagingDirectory.path()); + } finally { + stagingDirectory.close(); + } + } } private static boolean isCompatiblePlatform(String manifestPlatform, SupportedPlatform hostPlatform, @@ -278,45 +262,36 @@ private static File getReleaseFolder(Path cacheRoot, String releaseTag) { return cacheRoot.resolve(CUDA_FOLDERNAME).resolve(releaseTag).toFile(); } - static File getPlatformFolder(Path cacheRoot, String releaseTag, CudaPlatform platform) { - return getReleaseFolder(cacheRoot, releaseTag).toPath().resolve(platform.manifestName()).toFile(); - } - - static File getInstallationFolder(File platformFolder) { - return new File(platformFolder, CUDA_LIB_FOLDERNAME); - } - - static File getArchiveFile(File platformFolder, CudaPackage cudaPackage) { - var relativePath = cudaPackage.archive().relativePath(); - var archiveName = relativePath.substring(relativePath.lastIndexOf('/') + 1); - return new File(new File(new File(platformFolder, ARCHIVES_FOLDERNAME), cudaPackage.component()), archiveName); - } - - static NvidiaCudaManifest getManifest(File resourceFolder) { - return getManifest(getCacheRoot(resourceFolder), resourceFolder); + private static FileResourceProvider getManifestResource(String releaseTag) { + var manifestFilename = getManifestFilename(releaseTag); + return WebResourceProvider.newInstance(NVIDIA_REDIST_ROOT, manifestFilename, releaseTag); } - static NvidiaCudaManifest getManifest(Path cacheRoot, File resourceFolder) { - var releaseTag = ClangAstWebResource.getCudaReleaseTag(); + private static NvidiaCudaManifest downloadManifest(Path cacheRoot, Path stagingRoot, String releaseTag, + FileResourceProvider resource) { var manifestFilename = getManifestFilename(releaseTag); - var resource = WebResourceProvider.newInstance(NVIDIA_REDIST_ROOT, manifestFilename, releaseTag); - var manifestFile = CacheFiles.installFile(cacheRoot, new File(resourceFolder, manifestFilename), resource, null, + var manifestFile = CacheFiles.installFile(cacheRoot, stagingRoot.resolve(manifestFilename).toFile(), resource, null, "NVIDIA CUDA redistribution manifest"); var manifest = parseManifest(SpecsIo.read(manifestFile)); validateManifest(manifest, releaseTag); return manifest; } - private static Path getCacheRoot(File resourceFolder) { - var cacheRoot = resourceFolder.toPath(); - for (int i = 0; i < 3; i++) { - cacheRoot = cacheRoot.getParent(); - if (cacheRoot == null) { - throw new RuntimeException("CUDA resource folder is not below a cache root: '" + resourceFolder + "'"); - } + private static NvidiaCudaManifest readPublishedManifest(File releaseFolder, String releaseTag) { + var manifestPath = releaseFolder.toPath().resolve(getManifestFilename(releaseTag)); + if (!Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) { + throw invalidInstallation(releaseFolder, "published manifest"); } - return cacheRoot; + try { + var manifest = parseManifest(SpecsIo.read(manifestPath.toFile())); + validateManifest(manifest, releaseTag); + return manifest; + } catch (RuntimeException e) { + var invalid = invalidInstallation(releaseFolder, "published manifest"); + invalid.initCause(e); + throw invalid; + } } static String getManifestFilename(String releaseTag) { @@ -366,38 +341,36 @@ static NvidiaCudaManifest parseManifest(String json) { return new NvidiaCudaManifest(releaseDate, releaseLabel, releaseProduct, components); } - static File install(Path cacheRoot, File resourceFolder, String releaseTag, CudaPlatform platform, - NvidiaCudaManifest manifest, + static File install(Path cacheRoot, String releaseTag, FileResourceProvider manifestResource, Function archiveResourceFactory) { - validateManifest(manifest, releaseTag); + Objects.requireNonNull(manifestResource, "manifestResource"); Objects.requireNonNull(archiveResourceFactory, "archiveResourceFactory"); - var installationFolder = getInstallationFolder(resourceFolder); - if (Files.exists(installationFolder.toPath(), LinkOption.NOFOLLOW_LINKS)) { - return useExistingInstallation(cacheRoot, resourceFolder, platform, installationFolder); - } - - var downloadedPackages = manifest.getRequiredPackages(platform.manifestName()).stream() - .map(cudaPackage -> downloadPackage(cacheRoot, resourceFolder, cudaPackage, archiveResourceFactory)) - .toList(); - - CacheFiles.deleteUnlockedStagingLocks(cacheRoot, resourceFolder.toPath()); - var stagingDirectory = CacheFiles.createStagingDirectory(cacheRoot, resourceFolder.toPath(), ".cudalib.tmp-"); + var cudaRoot = cacheRoot.resolve(CUDA_FOLDERNAME); + var releaseFolder = getReleaseFolder(cacheRoot, releaseTag); + CacheFiles.deleteUnlockedStagingLocks(cacheRoot, cudaRoot); + var stagingDirectory = CacheFiles.createStagingDirectory(cacheRoot, cudaRoot, "." + releaseTag + ".tmp-"); try { + var manifest = downloadManifest(cacheRoot, stagingDirectory.path(), releaseTag, manifestResource); + var platform = requireSupportedPlatform(manifest); + try { - assemble(stagingDirectory.path().toFile(), platform, downloadedPackages); + prepareInstallation(stagingDirectory.path(), platform); + for (var cudaPackage : manifest.getRequiredPackages(platform.manifestName())) { + downloadAndAssemble(stagingDirectory.path(), cudaPackage, archiveResourceFactory); + } } catch (IOException e) { throw new UncheckedIOException("Could not assemble CUDA resources in '" + stagingDirectory.path() + "'", e); } - if (!isCudaInstallation(stagingDirectory.path().toFile(), platform.manifestName())) { + if (!isCudaInstallation(stagingDirectory.path().toFile(), releaseTag, platform.manifestName())) { throw new RuntimeException("Assembled CUDA resources failed structural validation in '" + stagingDirectory.path() + "'"); } - var publishedFolder = CacheFiles.publish(stagingDirectory.path(), installationFolder.toPath()).toFile(); - return useExistingInstallation(cacheRoot, resourceFolder, platform, publishedFolder); + CacheFiles.publish(stagingDirectory.path(), releaseFolder.toPath()); + return useExistingInstallation(cacheRoot, releaseFolder, releaseTag); } finally { try { CacheFiles.delete(stagingDirectory.path()); @@ -407,56 +380,78 @@ static File install(Path cacheRoot, File resourceFolder, String releaseTag, Cuda } } - private static CudaResources.DownloadedPackage downloadPackage(Path cacheRoot, File resourceFolder, - CudaPackage cudaPackage, - Function factory) { - var destination = getArchiveFile(resourceFolder, cudaPackage); - var archiveParent = destination.getParentFile().toPath(); - CacheFiles.deleteUnlockedStagingLocks(cacheRoot, archiveParent); - var archive = CacheFiles.installFile(cacheRoot, destination, factory.apply(cudaPackage), - cudaPackage.archive().sha256(), cudaPackage.archive().size(), - "NVIDIA CUDA archive '" + destination.getName() + "'"); - return new DownloadedPackage(cudaPackage, archive); + private static void downloadAndAssemble(Path stagingRoot, CudaPackage cudaPackage, + Function factory) throws IOException { + var downloadFolder = CacheFiles.createTemporaryDirectory(stagingRoot, ".download-"); + try { + var archive = factory.apply(cudaPackage).write(downloadFolder.toFile()); + var archiveName = getArchiveName(cudaPackage); + if (archive == null || !archive.isFile()) { + throw new RuntimeException("Could not download NVIDIA CUDA archive '" + archiveName + "'"); + } + + var expectedSize = cudaPackage.archive().size(); + if (Files.size(archive.toPath()) != expectedSize) { + throw new RuntimeException("Downloaded NVIDIA CUDA archive '" + archiveName + + "' does not match expected size '" + expectedSize + "' (actual: " + + Files.size(archive.toPath()) + ")"); + } + + var expectedSha256 = cudaPackage.archive().sha256(); + if (!CacheFiles.hasExpectedSha256(archive, expectedSha256)) { + throw new RuntimeException("Downloaded NVIDIA CUDA archive '" + archiveName + + "' does not match expected SHA-256 '" + expectedSha256 + "'"); + } + + assemblePackage(stagingRoot.toFile(), cudaPackage, archive); + } finally { + CacheFiles.delete(downloadFolder); + } + } + + private static String getArchiveName(CudaPackage cudaPackage) { + var relativePath = cudaPackage.archive().relativePath(); + return relativePath.substring(relativePath.lastIndexOf('/') + 1); } - private static File useExistingInstallation(Path cacheRoot, File resourceFolder, CudaPlatform platform, - File installationFolder) { + private static File useExistingInstallation(Path cacheRoot, File releaseFolder, String releaseTag) { var validInstallation = CacheFiles.withMaintenanceLock(cacheRoot, () -> { - if (!isCudaInstallation(installationFolder, platform.manifestName())) { - throw invalidInstallation(installationFolder, platform.manifestName()); + var platform = requireSupportedPlatform(readPublishedManifest(releaseFolder, releaseTag)); + if (!isCudaInstallation(releaseFolder, releaseTag, platform.manifestName())) { + throw invalidInstallation(releaseFolder, platform.manifestName()); } - CacheFiles.touch(resourceFolder.toPath()); - CacheFiles.touch(resourceFolder.getParentFile().toPath()); - return installationFolder; + CacheFiles.touch(releaseFolder.toPath()); + return releaseFolder; }); - cleanup(cacheRoot, resourceFolder); + cleanup(cacheRoot, releaseFolder.toPath()); SpecsLogs.debug(() -> "Using cached CUDA resources: " + validInstallation); return validInstallation; } - private static void cleanup(Path cacheRoot, File resourceFolder) { + private static void cleanup(Path cacheRoot, Path releaseFolder) { var cudaRoot = cacheRoot.resolve(CUDA_FOLDERNAME); - var releaseFolder = resourceFolder.toPath().getParent(); var cutoff = Instant.now().minus(Duration.ofDays(60)); try { CacheFiles.deleteStaleDirectories(cacheRoot, cudaRoot, cutoff, releaseFolder); - CacheFiles.deleteUnlockedStagingLocks(cacheRoot, releaseFolder); - CacheFiles.deleteUnlockedStagingLocks(cacheRoot, resourceFolder.toPath()); - for (var component : REQUIRED_COMPONENTS) { - CacheFiles.deleteUnlockedStagingLocks(cacheRoot, - resourceFolder.toPath().resolve(ARCHIVES_FOLDERNAME).resolve(component)); - } + CacheFiles.deleteUnlockedStagingLocks(cacheRoot, cudaRoot); } catch (RuntimeException e) { SpecsLogs.warn("Could not clean stale CUDA cache resources", e); } } - static boolean isCudaInstallation(File folder, String platform) { + static boolean isCudaInstallation(File folder, String releaseTag, String platform) { Path root = folder.toPath().toAbsolutePath().normalize(); if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS) - || !Files.isDirectory(root.resolve("bin"), LinkOption.NOFOLLOW_LINKS)) { + || !Files.isDirectory(root.resolve("bin"), LinkOption.NOFOLLOW_LINKS) + || !Files.isDirectory(root.resolve("include"), LinkOption.NOFOLLOW_LINKS) + || !Files.isDirectory(root.resolve("nvvm"), LinkOption.NOFOLLOW_LINKS)) { + return false; + } + + var manifestFilename = getManifestFilename(releaseTag); + if (!Files.isRegularFile(root.resolve(manifestFilename), LinkOption.NOFOLLOW_LINKS)) { return false; } @@ -472,7 +467,14 @@ static boolean isCudaInstallation(File folder, String platform) { } try { - return platform.equals(Files.readString(platformFile).trim()); + if (!platform.equals(Files.readString(platformFile).trim())) { + return false; + } + + var expectedEntries = Set.of("bin", "include", "nvvm", PLATFORM_FILENAME, manifestFilename); + try (var entries = Files.list(root)) { + return entries.allMatch(entry -> expectedEntries.contains(entry.getFileName().toString())); + } } catch (IOException e) { return false; } @@ -583,22 +585,30 @@ private static void validateRelativePath(String relativePath, String owner) { } } + private static void prepareInstallation(Path stagingFolder, CudaPlatform platform) throws IOException { + Files.writeString(stagingFolder.resolve(PLATFORM_FILENAME), platform.manifestName()); + Files.createDirectories(stagingFolder.resolve("bin")); + } + static void assemble(File stagingFolder, CudaPlatform platform, List packages) throws IOException { - Files.writeString(new File(stagingFolder, PLATFORM_FILENAME).toPath(), platform.manifestName()); - Files.createDirectories(new File(stagingFolder, "bin").toPath()); + prepareInstallation(stagingFolder.toPath(), platform); for (var downloadedPackage : packages) { - var component = downloadedPackage.cudaPackage().component(); - var sourceRoots = switch (component) { - case "cuda_cudart", "libcurand", "cuda_cccl" -> List.of("include"); - case "cuda_nvcc" -> List.of("include/crt", "nvvm/libdevice/libdevice.10.bc"); - default -> throw new RuntimeException("Unsupported NVIDIA CUDA component '" + component + "'"); - }; - - extractArchive(downloadedPackage.archiveFile(), stagingFolder, sourceRoots); + assemblePackage(stagingFolder, downloadedPackage.cudaPackage(), downloadedPackage.archiveFile()); } } + private static void assemblePackage(File stagingFolder, CudaPackage cudaPackage, File archive) throws IOException { + var sourceRoots = switch (cudaPackage.component()) { + case "cuda_cudart", "libcurand", "cuda_cccl" -> List.of("include"); + case "cuda_nvcc" -> List.of("include/crt", "nvvm/libdevice/libdevice.10.bc"); + default -> throw new RuntimeException("Unsupported NVIDIA CUDA component '" + + cudaPackage.component() + "'"); + }; + + extractArchive(archive, stagingFolder, sourceRoots); + } + private static void extractArchive(File archive, File destination, List sourceRoots) throws IOException { if (archive.getName().endsWith(".zip")) { try (InputStream input = Files.newInputStream(archive.toPath()); diff --git a/ClangAstParser/test/pt/up/fe/specs/clang/ClangResourcesTest.java b/ClangAstParser/test/pt/up/fe/specs/clang/ClangResourcesTest.java index 3169d7b77..79ea3ff68 100644 --- a/ClangAstParser/test/pt/up/fe/specs/clang/ClangResourcesTest.java +++ b/ClangAstParser/test/pt/up/fe/specs/clang/ClangResourcesTest.java @@ -542,13 +542,18 @@ public void builtinCudaAutoSystemLibcUsesSameResourcePathAsExplicitSystem() { } @Test - public void builtinCudaArchiveHasCanonicalInstallationLayout() { + public void builtinCudaInstallationHasCanonicalLayout() { var parser = newParser(CodeParser.getBuiltinOption()); var cudaFolder = new ClangResources(parser).getBuiltinCudaLib(); - var cudaPlatform = cudaFolder.getParentFile().getName(); assertEquals(tempFolder.resolve("cuda").resolve(ClangAstWebResource.getCudaReleaseTag()) - .resolve(cudaPlatform).resolve("cudalib").toFile().getAbsolutePath(), cudaFolder.getAbsolutePath()); + .toFile().getAbsolutePath(), cudaFolder.getAbsolutePath()); + assertTrue(new File(cudaFolder, CudaResources.getManifestFilename(ClangAstWebResource.getCudaReleaseTag())) + .isFile()); + assertTrue(new File(cudaFolder, CudaResources.PLATFORM_FILENAME).isFile()); + assertFalse(new File(cudaFolder, "archives").exists()); + assertFalse(new File(cudaFolder, "cudalib").exists()); + assertFalse(new File(cudaFolder, "linux-x86_64").exists()); assertTrue(new File(cudaFolder, "include/cuda.h").isFile()); assertTrue(new File(cudaFolder, "include/cuda_runtime.h").isFile()); assertTrue(new File(cudaFolder, "nvvm/libdevice/libdevice.10.bc").isFile()); diff --git a/ClangAstParser/test/pt/up/fe/specs/clang/CudaResourcesTest.java b/ClangAstParser/test/pt/up/fe/specs/clang/CudaResourcesTest.java index 09f27952d..87d96f502 100644 --- a/ClangAstParser/test/pt/up/fe/specs/clang/CudaResourcesTest.java +++ b/ClangAstParser/test/pt/up/fe/specs/clang/CudaResourcesTest.java @@ -20,7 +20,6 @@ import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import pt.up.fe.specs.clang.codeparser.CodeParser; import pt.up.fe.specs.util.providers.FileResourceProvider; import java.io.File; @@ -38,7 +37,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -86,8 +84,7 @@ public void manifestSelectsRequiredComponentsAndValidatesMetadata() { var wrongRelease = CudaResources.parseManifest(manifestJson() .replace("\"release_label\": \"12.3.2\"", "\"release_label\": \"12.3.1\"")); var releaseError = assertThrows(RuntimeException.class, () -> CudaResources.install( - tempFolder, tempFolder.resolve("wrong-release").toFile(), RELEASE, - new CudaResources.CudaPlatform(PLATFORM), wrongRelease, ignored -> { + tempFolder, RELEASE, manifestResource(manifestJson(wrongRelease, wrongRelease.releaseLabel())), ignored -> { throw new AssertionError("Archive downloads must not start for an invalid manifest"); })); assertTrue(releaseError.getMessage().contains("release label")); @@ -95,8 +92,7 @@ public void manifestSelectsRequiredComponentsAndValidatesMetadata() { var wrongProduct = CudaResources.parseManifest(manifestJson() .replace("\"release_product\": \"cuda\"", "\"release_product\": \"other\"")); var productError = assertThrows(RuntimeException.class, () -> CudaResources.install( - tempFolder, tempFolder.resolve("wrong-product").toFile(), RELEASE, - new CudaResources.CudaPlatform(PLATFORM), wrongProduct, ignored -> { + tempFolder, RELEASE, manifestResource(manifestJson(wrongProduct, RELEASE)), ignored -> { throw new AssertionError("Archive downloads must not start for an invalid manifest"); })); assertTrue(productError.getMessage().contains("not a CUDA manifest")); @@ -140,28 +136,32 @@ public void supportDetectionReturnsFalseOnlyForAValidatedUnsupportedHost() throw var unsupportedPlatform = SupportedPlatform.getCurrentPlatform().isWindows() ? "linux-riscv64" : "windows-x86_64"; - writeCachedManifest(manifestJson(unsupportedPlatform)); + var manifestResource = manifestResource(manifestJson(unsupportedPlatform)); - assertFalse(CudaResources.isSupportedPlatform(tempFolder)); + assertFalse(CudaResources.isSupportedPlatform(tempFolder, manifestResource)); + assertFalse(Files.exists(tempFolder.resolve("cuda").resolve(RELEASE))); + assertNoCudaStaging(); } @Test public void supportDetectionPropagatesManifestAndCacheFailures() throws IOException { - writeCachedManifest("not-json"); - assertThrows(RuntimeException.class, () -> CudaResources.isSupportedPlatform(tempFolder)); + assertThrows(RuntimeException.class, + () -> CudaResources.isSupportedPlatform(tempFolder, manifestResource("not-json"))); + assertNoCudaStaging(); var invalidManifestRoot = Files.createDirectory(tempFolder.resolve("invalid")).toAbsolutePath(); - writeCachedManifest(invalidManifestRoot, manifestJson() - .replace("\"release_product\": \"cuda\"", "\"release_product\": \"other\"")); - assertThrows(RuntimeException.class, () -> CudaResources.isSupportedPlatform(invalidManifestRoot)); + assertThrows(RuntimeException.class, () -> CudaResources.isSupportedPlatform(invalidManifestRoot, + manifestResource(manifestJson() + .replace("\"release_product\": \"cuda\"", "\"release_product\": \"other\"")))); var missingComponentRoot = Files.createDirectory(tempFolder.resolve("missing")).toAbsolutePath(); - writeCachedManifest(missingComponentRoot, manifestJson().replace("\"cuda_cccl\":", "\"missing\":")); - assertThrows(RuntimeException.class, () -> CudaResources.isSupportedPlatform(missingComponentRoot)); + assertThrows(RuntimeException.class, () -> CudaResources.isSupportedPlatform(missingComponentRoot, + manifestResource(manifestJson().replace("\"cuda_cccl\":", "\"missing\":")))); var cacheFile = tempFolder.resolve("cache-file"); Files.writeString(cacheFile, "not-a-directory"); - assertThrows(RuntimeException.class, () -> CudaResources.isSupportedPlatform(cacheFile)); + assertThrows(RuntimeException.class, () -> CudaResources.isSupportedPlatform(cacheFile, + manifestResource(manifestJson()))); } @Test @@ -177,19 +177,27 @@ public void additionalCompatibleManifestPlatformNeedsNoJavaSupportWhitelist() { public void installationFetchesOnlyTheSelectedPlatformArchives() throws IOException { var archives = createArchives(); var manifest = addUnusedPlatforms(archives.manifest()); - var platform = new CudaResources.CudaPlatform(PLATFORM); - var platformFolder = CudaResources.getPlatformFolder(tempFolder, RELEASE, platform); var writes = new AtomicInteger(); - var installation = CudaResources.install(tempFolder, platformFolder, RELEASE, platform, manifest, + var installation = CudaResources.install(tempFolder, RELEASE, manifestResource(manifestJson(manifest, RELEASE)), cudaPackage -> { assertTrue(cudaPackage.archive().relativePath().contains("/" + PLATFORM + "/")); var source = archives.files().get(cudaPackage.component()); return copyingResource(source, source.getFileName().toString(), writes); }); - assertTrue(CudaResources.isCudaInstallation(installation, PLATFORM)); + assertEquals(tempFolder.resolve("cuda").resolve(RELEASE).toFile().getAbsoluteFile(), installation.getAbsoluteFile()); + assertTrue(CudaResources.isCudaInstallation(installation, RELEASE, PLATFORM)); assertEquals(CudaResources.REQUIRED_COMPONENTS.size(), writes.get()); + assertTrue(Files.isRegularFile(installation.toPath().resolve(CudaResources.getManifestFilename(RELEASE)))); + assertTrue(Files.isRegularFile(installation.toPath().resolve(CudaResources.PLATFORM_FILENAME))); + assertTrue(Files.isDirectory(installation.toPath().resolve("bin"))); + assertTrue(Files.isDirectory(installation.toPath().resolve("include"))); + assertTrue(Files.isDirectory(installation.toPath().resolve("nvvm"))); + assertFalse(Files.exists(installation.toPath().resolve("archives"))); + assertFalse(Files.exists(installation.toPath().resolve("cudalib"))); + assertFalse(Files.exists(installation.toPath().resolve(PLATFORM))); + assertNoCudaStaging(); } @Test @@ -197,25 +205,46 @@ public void archiveDownloadsRequireBothExpectedSizeAndSha256() throws IOExceptio var source = Files.writeString(tempFolder.resolve("cuda_cudart.tar.xz"), "archive"); var actualSize = Files.size(source); var actualSha = sha256(source); - var platformFolder = CudaResources.getPlatformFolder(tempFolder, RELEASE, new CudaResources.CudaPlatform(PLATFORM)); var wrongSize = manifestForSingleArchive(source, actualSha, actualSize + 1); var sizeError = assertThrows(RuntimeException.class, - () -> install(wrongSize, platformFolder, source, new AtomicInteger())); + () -> install(wrongSize, RELEASE, source, new AtomicInteger())); assertTrue(sizeError.getMessage().contains("expected size")); - assertFalse(CudaResources.getArchiveFile(platformFolder, - wrongSize.getRequiredPackages(PLATFORM).get(0)).isFile()); + assertFalse(Files.exists(tempFolder.resolve("cuda").resolve(RELEASE))); + assertNoCudaStaging(); var wrongSha = manifestForSingleArchive(source, "0".repeat(64), actualSize); var shaError = assertThrows(RuntimeException.class, - () -> install(wrongSha, platformFolder, source, new AtomicInteger())); + () -> install(wrongSha, RELEASE, source, new AtomicInteger())); assertTrue(shaError.getMessage().contains("expected SHA-256")); + assertFalse(Files.exists(tempFolder.resolve("cuda").resolve(RELEASE))); + assertNoCudaStaging(); + } + + @Test + public void failedInstallationRemovesStagingAndDoesNotPublish() throws Exception { + var archives = createArchives(); + var error = assertThrows(RuntimeException.class, () -> CudaResources.install( + tempFolder, RELEASE, manifestResource(manifestJson(archives.manifest(), RELEASE)), cudaPackage -> { + if (cudaPackage.component().equals("libcurand")) { + throw new RuntimeException("download failed"); + } + + var source = archives.files().get(cudaPackage.component()); + return copyingResource(source, source.getFileName().toString(), new AtomicInteger()); + })); + + assertEquals("download failed", error.getMessage()); + assertFalse(Files.exists(tempFolder.resolve("cuda").resolve(RELEASE))); + assertNoCudaStaging(); } @Test public void assembleSupportsTarXzAndZipPackages() throws IOException { var archives = createArchives(); - var stagingFolder = Files.createDirectory(tempFolder.resolve("cudalib")); + var stagingFolder = Files.createDirectory(tempFolder.resolve("cuda-staging")); + Files.writeString(stagingFolder.resolve(CudaResources.getManifestFilename(RELEASE)), + manifestJson(archives.manifest(), RELEASE)); CudaResources.assemble(stagingFolder.toFile(), new CudaResources.CudaPlatform(PLATFORM), downloadedPackages(archives)); @@ -231,7 +260,7 @@ public void assembleSupportsTarXzAndZipPackages() throws IOException { assertEquals("libdevice", Files.readString(stagingFolder.resolve("nvvm/libdevice/libdevice.10.bc"))); assertFalse(Files.exists(stagingFolder.resolve("bin/discarded"))); assertFalse(Files.exists(stagingFolder.resolve("bin/discarded.exe"))); - assertTrue(CudaResources.isCudaInstallation(stagingFolder.toFile(), PLATFORM)); + assertTrue(CudaResources.isCudaInstallation(stagingFolder.toFile(), RELEASE, PLATFORM)); } @Test @@ -263,116 +292,79 @@ public void extractionRejectsTraversalEntries() throws IOException { @Test public void requiredComponentsAreStoredPerReleaseWithoutDeduplication() throws IOException { var archives = createArchives(); - var firstRelease = CudaResources.getPlatformFolder(tempFolder, RELEASE, - new CudaResources.CudaPlatform(PLATFORM)); - var secondRelease = CudaResources.getPlatformFolder(tempFolder, "13.3.1", - new CudaResources.CudaPlatform(PLATFORM)); + var firstRelease = tempFolder.resolve("cuda").resolve(RELEASE); + var secondRelease = tempFolder.resolve("cuda").resolve("13.3.1"); - var first = install(archives, firstRelease, new AtomicInteger()); - var second = install(archives, secondRelease, new AtomicInteger()); + var first = install(archives, RELEASE, new AtomicInteger()); + var second = install(archives, "13.3.1", new AtomicInteger()); - assertTrue(CudaResources.isCudaInstallation(first, PLATFORM)); - assertTrue(CudaResources.isCudaInstallation(second, PLATFORM)); - assertNotEquals(CudaResources.getArchiveFile(firstRelease, - archives.manifest().getRequiredPackages(PLATFORM).get(0)).toPath(), - CudaResources.getArchiveFile(secondRelease, - archives.manifest().getRequiredPackages(PLATFORM).get(0)).toPath()); + assertEquals(firstRelease.toFile().getAbsoluteFile(), first.getAbsoluteFile()); + assertEquals(secondRelease.toFile().getAbsoluteFile(), second.getAbsoluteFile()); + assertNotEquals(first.getAbsoluteFile(), second.getAbsoluteFile()); + assertTrue(CudaResources.isCudaInstallation(first, RELEASE, PLATFORM)); + assertTrue(CudaResources.isCudaInstallation(second, "13.3.1", PLATFORM)); + assertFalse(Files.exists(first.toPath().resolve("archives"))); + assertFalse(Files.exists(second.toPath().resolve("archives"))); } @Test public void existingValidInstallationIsReusedAndUsageIsRefreshed() throws IOException { - var platform = new CudaResources.CudaPlatform(hostPlatform()); - var platformFolder = CudaResources.getPlatformFolder(tempFolder, RELEASE, platform); - var installation = CudaResources.getInstallationFolder(platformFolder); - writeValidInstallation(installation.toPath(), PLATFORM); + var releaseFolder = tempFolder.resolve("cuda").resolve(RELEASE); + writeValidInstallation(releaseFolder, PLATFORM); var old = FileTime.from(Instant.now().minus(Duration.ofDays(61))); - Files.setLastModifiedTime(platformFolder.toPath().getParent(), old); + Files.setLastModifiedTime(releaseFolder, old); - var parser = CodeParser.newInstance(); - parser.set(CodeParser.DUMPER_FOLDER, tempFolder.toFile()); - var result = new ClangResources(parser).getBuiltinCudaLib(); + var result = CudaResources.getBuiltinCudaLib(tempFolder); - assertEquals(installation.getAbsoluteFile(), result.getAbsoluteFile()); - assertTrue(Files.getLastModifiedTime(platformFolder.toPath().getParent()).toInstant() + assertEquals(releaseFolder.toFile().getAbsoluteFile(), result.getAbsoluteFile()); + assertTrue(Files.getLastModifiedTime(releaseFolder).toInstant() .isAfter(Instant.now().minus(Duration.ofDays(1)))); } @Test public void invalidPublishedInstallationFailsWithoutRepair() throws IOException { - var platform = new CudaResources.CudaPlatform(hostPlatform()); - var platformFolder = CudaResources.getPlatformFolder(tempFolder, RELEASE, platform); - var installation = CudaResources.getInstallationFolder(platformFolder); - Files.createDirectories(installation.toPath()); - Files.writeString(installation.toPath().resolve("sentinel"), "do not repair"); + var releaseFolder = tempFolder.resolve("cuda").resolve(RELEASE); + Files.createDirectories(releaseFolder); + Files.writeString(releaseFolder.resolve(CudaResources.getManifestFilename(RELEASE)), manifestJson()); + Files.writeString(releaseFolder.resolve("sentinel"), "do not repair"); - var parser = CodeParser.newInstance(); - parser.set(CodeParser.DUMPER_FOLDER, tempFolder.toFile()); - var error = assertThrows(RuntimeException.class, () -> new ClangResources(parser).getBuiltinCudaLib()); + var error = assertThrows(RuntimeException.class, () -> CudaResources.getBuiltinCudaLib(tempFolder)); - assertTrue(error.getMessage().contains(installation.getAbsolutePath())); + assertTrue(error.getMessage().contains(releaseFolder.toAbsolutePath().toString())); assertTrue(error.getMessage().contains("delete this directory manually to regenerate")); - assertEquals("do not repair", Files.readString(installation.toPath().resolve("sentinel"))); + assertEquals("do not repair", Files.readString(releaseFolder.resolve("sentinel"))); } @Test - public void oldPartialReleaseIsProtectedWhileInitializationContinues() throws Exception { - var platformFolder = CudaResources.getPlatformFolder(tempFolder, RELEASE, - new CudaResources.CudaPlatform(PLATFORM)); - var releaseFolder = platformFolder.toPath().getParent(); - Files.createDirectories(platformFolder.toPath().resolve("partial")); - Files.writeString(platformFolder.toPath().resolve("partial/manifest-download"), "in progress"); - var old = FileTime.from(Instant.now().minus(Duration.ofDays(61))); - Files.setLastModifiedTime(releaseFolder, old); - Files.setLastModifiedTime(platformFolder.toPath(), old); + public void abandonedStagingDirectoriesAreReclaimable() throws Exception { + var cudaRoot = Files.createDirectories(tempFolder.resolve("cuda")); + var staging = CacheFiles.createStagingDirectory(tempFolder, cudaRoot, "." + RELEASE + ".tmp-"); + var stagingPath = staging.path(); + var lockPath = staging.lockPath(); + Files.writeString(stagingPath.resolve("partial"), "in progress"); + staging.close(); + Files.createFile(lockPath); - var claimed = new CountDownLatch(1); - var allowInitializationToFinish = new CountDownLatch(1); - var executor = Executors.newFixedThreadPool(2); + CacheFiles.deleteUnlockedStagingLocks(tempFolder, cudaRoot); - try { - var initialization = executor.submit(() -> { - CudaResources.claimInUse(tempFolder, platformFolder); - claimed.countDown(); - awaitLatch(allowInitializationToFinish); - }); - assertTrue(claimed.await(TEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)); - - var cleanup = executor.submit(() -> CacheFiles.deleteStaleDirectories(tempFolder, - tempFolder.resolve("cuda"), Instant.now().minus(Duration.ofDays(60)), null)); - cleanup.get(TEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); - - assertTrue(Files.isDirectory(releaseFolder)); - assertTrue(Files.isDirectory(platformFolder.toPath())); - assertTrue(Files.getLastModifiedTime(releaseFolder).toInstant() - .isAfter(Instant.now().minus(Duration.ofDays(1)))); - assertTrue(Files.getLastModifiedTime(platformFolder.toPath()).toInstant() - .isAfter(Instant.now().minus(Duration.ofDays(1)))); - - allowInitializationToFinish.countDown(); - initialization.get(TEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); - } finally { - allowInitializationToFinish.countDown(); - executor.shutdownNow(); - executor.awaitTermination(TEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); - } + assertFalse(Files.exists(stagingPath)); + assertFalse(Files.exists(lockPath)); } @Test public void concurrentPublicationLeavesOneValidInstallation() throws Exception { var archives = createArchives(); - var platform = new CudaResources.CudaPlatform(PLATFORM); - var platformFolder = CudaResources.getPlatformFolder(tempFolder, RELEASE, platform); var writes = new AtomicInteger(); var executor = Executors.newFixedThreadPool(4); var futures = new ArrayList>(); try { for (int i = 0; i < 4; i++) { - futures.add(executor.submit(() -> install(archives, platformFolder, writes))); + futures.add(executor.submit(() -> install(archives, RELEASE, writes))); } for (var future : futures) { - assertEquals(CudaResources.getInstallationFolder(platformFolder).getAbsoluteFile(), + assertEquals(tempFolder.resolve("cuda").resolve(RELEASE).toFile().getAbsoluteFile(), future.get(TEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS).getAbsoluteFile()); } } finally { @@ -380,47 +372,36 @@ public void concurrentPublicationLeavesOneValidInstallation() throws Exception { executor.awaitTermination(TEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); } - assertTrue(CudaResources.isCudaInstallation(CudaResources.getInstallationFolder(platformFolder), PLATFORM)); - try (var children = Files.list(platformFolder.toPath())) { - assertTrue(children.noneMatch(path -> path.getFileName().toString().startsWith(".cudalib.tmp-"))); - } - try (var children = Files.list(platformFolder.toPath().resolve("archives/cuda_cudart"))) { - assertTrue(children.noneMatch(path -> path.getFileName().toString().startsWith(".cuda_cudart"))); - } + var releaseFolder = tempFolder.resolve("cuda").resolve(RELEASE); + assertTrue(CudaResources.isCudaInstallation(releaseFolder.toFile(), RELEASE, PLATFORM)); + assertNoCudaStaging(); assertTrue(writes.get() >= 4); } @Test public void staleCudaReleasesAreRemovedAfterSixtyDays() throws IOException { - var platform = new CudaResources.CudaPlatform(hostPlatform()); - var currentFolder = CudaResources.getPlatformFolder(tempFolder, RELEASE, platform); - var currentInstallation = CudaResources.getInstallationFolder(currentFolder); - writeValidInstallation(currentInstallation.toPath(), PLATFORM); - - var staleFolder = CudaResources.getPlatformFolder(tempFolder, "11.8.0", platform); - Files.createDirectories(staleFolder.toPath()); - Files.setLastModifiedTime(staleFolder.toPath().getParent(), + var currentFolder = tempFolder.resolve("cuda").resolve(RELEASE); + writeValidInstallation(currentFolder, PLATFORM); + + var staleFolder = tempFolder.resolve("cuda").resolve("11.8.0"); + Files.createDirectories(staleFolder); + Files.setLastModifiedTime(staleFolder, FileTime.from(Instant.now().minus(Duration.ofDays(61)))); - var parser = CodeParser.newInstance(); - parser.set(CodeParser.DUMPER_FOLDER, tempFolder.toFile()); - new ClangResources(parser).getBuiltinCudaLib(); + CudaResources.getBuiltinCudaLib(tempFolder); - assertTrue(currentInstallation.isDirectory()); - assertFalse(staleFolder.getParentFile().exists()); + assertTrue(Files.isDirectory(currentFolder)); + assertFalse(Files.exists(staleFolder)); } - private File install(CudaResources.NvidiaCudaManifest manifest, File platformFolder, Path source, - AtomicInteger writes) { - return CudaResources.install(tempFolder, platformFolder, RELEASE, - new CudaResources.CudaPlatform(PLATFORM), manifest, - cudaPackage -> copyingResource(source, - source.getFileName().toString(), writes)); + private File install(CudaResources.NvidiaCudaManifest manifest, String releaseTag, Path source, + AtomicInteger writes) throws IOException { + return CudaResources.install(tempFolder, releaseTag, manifestResource(manifestJson(manifest, releaseTag)), + cudaPackage -> copyingResource(source, source.getFileName().toString(), writes)); } - private File install(ArchiveSet archives, File platformFolder, AtomicInteger writes) { - return CudaResources.install(tempFolder, platformFolder, RELEASE, - new CudaResources.CudaPlatform(PLATFORM), archives.manifest(), + private File install(ArchiveSet archives, String releaseTag, AtomicInteger writes) throws IOException { + return CudaResources.install(tempFolder, releaseTag, manifestResource(manifestJson(archives.manifest(), releaseTag)), cudaPackage -> { var source = archives.files().get(cudaPackage.component()); return copyingResource(source, source.getFileName().toString(), writes); @@ -504,6 +485,7 @@ private CudaResources.NvidiaCudaManifest addUnusedPlatforms(CudaResources.Nvidia private void writeValidInstallation(Path installation, String platform) throws IOException { Files.createDirectories(installation.resolve("bin")); + Files.writeString(installation.resolve(CudaResources.getManifestFilename(RELEASE)), manifestJson()); Files.writeString(installation.resolve(CudaResources.PLATFORM_FILENAME), platform); for (var requiredFile : List.of( "include/cuda.h", @@ -519,6 +501,27 @@ private void writeValidInstallation(Path installation, String platform) throws I } } + private FileResourceProvider manifestResource(String json) { + try { + var source = Files.createTempFile(tempFolder, "manifest-", ".json"); + Files.writeString(source, json); + return copyingResource(source, source.getFileName().toString(), new AtomicInteger()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void assertNoCudaStaging() throws IOException { + var cudaRoot = tempFolder.resolve("cuda"); + if (!Files.isDirectory(cudaRoot)) { + return; + } + + try (var children = Files.list(cudaRoot)) { + assertTrue(children.noneMatch(path -> path.getFileName().toString().contains(".tmp-"))); + } + } + private FileResourceProvider copyingResource(Path source, String filename, AtomicInteger writes) { return new FileResourceProvider() { @Override @@ -545,20 +548,6 @@ public String getFilename() { }; } - private static String hostPlatform() { - return PLATFORM; - } - - private void writeCachedManifest(String json) throws IOException { - writeCachedManifest(tempFolder, json); - } - - private void writeCachedManifest(Path cacheRoot, String json) throws IOException { - var releaseFolder = cacheRoot.resolve("cuda").resolve(ClangAstWebResource.getCudaReleaseTag()); - Files.createDirectories(releaseFolder); - Files.writeString(releaseFolder.resolve(CudaResources.getManifestFilename(RELEASE)), json); - } - private static void writeTarXz(Path archive, Map files) throws IOException { try (OutputStream output = Files.newOutputStream(archive); var xzOutput = new XZCompressorOutputStream(output); @@ -586,6 +575,37 @@ private static void writeZip(Path archive, Map files) throws IOE } } + private static String manifestJson(CudaResources.NvidiaCudaManifest manifest, String releaseTag) { + return """ + { + "release_date": "%s", + "release_label": "%s", + "release_product": "%s", + "cuda_cudart": %s, + "cuda_nvcc": %s, + "libcurand": %s, + "cuda_cccl": %s + } + """.formatted( + manifest.releaseDate(), releaseTag, manifest.releaseProduct(), + componentJson(manifest.components().get("cuda_cudart")), + componentJson(manifest.components().get("cuda_nvcc")), + componentJson(manifest.components().get("libcurand")), + componentJson(manifest.components().get("cuda_cccl"))); + } + + private static String componentJson(CudaResources.NvidiaCudaComponent component) { + var platforms = new ArrayList(); + for (var entry : component.archives().entrySet()) { + var archive = entry.getValue(); + platforms.add("\"%s\": {\"relative_path\": \"%s\", \"sha256\": \"%s\", \"size\": \"%d\"}" + .formatted(entry.getKey(), archive.relativePath(), archive.sha256(), archive.size())); + } + + return "{\"name\": \"%s\", \"version\": \"%s\", %s}" + .formatted(component.name(), component.version(), String.join(", ", platforms)); + } + private static String manifestJson() { return manifestJson(PLATFORM); } @@ -637,17 +657,6 @@ private static String sha256(Path file) throws IOException { private static final String SHA256 = "0".repeat(64); - private static void awaitLatch(CountDownLatch latch) { - try { - if (!latch.await(TEST_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)) { - throw new AssertionError("Timed out waiting for CUDA initialization test coordination"); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - } - private record ArchiveSet(CudaResources.NvidiaCudaManifest manifest, Map files) { } }