diff --git a/vadl/build.gradle.kts b/vadl/build.gradle.kts index 08b320c79..a8d44214f 100644 --- a/vadl/build.gradle.kts +++ b/vadl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation("org.apache.commons:commons-text:1.10.0") implementation("io.github.rascmatt:z3-bootstrap:1.0.0") + testImplementation("io.github.kper:buildkitcli:0.12.0") testCompileOnly(project(":java-annotations")) testImplementation(platform("org.junit:junit-bom:5.11.4")) diff --git a/vadl/main/resources/templates/lcb/LcbMakeFile b/vadl/main/resources/templates/lcb/LcbMakeFile index 1114258d6..7ef0ed868 100644 --- a/vadl/main/resources/templates/lcb/LcbMakeFile +++ b/vadl/main/resources/templates/lcb/LcbMakeFile @@ -46,9 +46,7 @@ build: -DLLVM_PARALLEL_COMPILE_JOBS=$(LLVM_PARALLEL_COMPILE_JOBS) \ -DLLVM_PARALLEL_LINK_JOBS=$(LLVM_PARALLEL_LINK_JOBS) \ -DLLVM_CCACHE_BUILD=$(LLVM_CCACHE_BUILD) \ - -DLLVM_ENABLE_ASSERTIONS=$(LLVM_ENABLE_ASSERTIONS) \ - -DCMAKE_C_COMPILER_LAUNCHER=sccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + -DLLVM_ENABLE_ASSERTIONS=$(LLVM_ENABLE_ASSERTIONS) cmake --build $(LLVM_SOURCE_PATH)/build build-llc: diff --git a/vadl/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/vadl/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 000000000..1e2e2239f --- /dev/null +++ b/vadl/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +vadl.RemovingDockerImagesTeardown \ No newline at end of file diff --git a/vadl/test/resources/images/lcb_execution_test_aarch64temp/Dockerfile b/vadl/test/resources/images/lcb_execution_test_aarch64temp/Dockerfile index 26a2d06ff..bde31950d 100644 --- a/vadl/test/resources/images/lcb_execution_test_aarch64temp/Dockerfile +++ b/vadl/test/resources/images/lcb_execution_test_aarch64temp/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.7 FROM ghcr.io/openvadl/lcb-execution-test-aarch64-base-image@sha256:78d418708da2656805f7bac1204a1adc55f5a72f749b6d2100e275e176c816cf ARG TARGET ARG SCCACHE_REDIS_ENDPOINT @@ -5,6 +6,7 @@ ARG UPSTREAM_BUILD_TARGET ARG UPSTREAM_CLANG_TARGET ARG SPIKE_TARGET ARG ABI +LABEL key=VADL_TEST_CONTAINER WORKDIR /llvm/build ENV TARGET=${TARGET} ENV UPSTREAM_CLANG_TARGET=${UPSTREAM_CLANG_TARGET} @@ -12,9 +14,10 @@ ENV LLVM_UPSTREAM_TARGETS=${UPSTREAM_BUILD_TARGET} ENV SCCACHE_REDIS_ENDPOINT=${SCCACHE_REDIS_ENDPOINT} ENV SPIKE_TARGET=${SPIKE_TARGET} ENV ABI=${ABI} +ENV SCCACHE_DIR=/root/.cache/sccache ENV PATH=/opt/aarch64/bin:/opt/qemu/bin:$PATH COPY . /src WORKDIR /src -RUN make && sccache -s +RUN --mount=type=cache,target=/root/.cache/sccache make && sccache -s RUN mkdir /output -CMD sh /work/compile.sh \ No newline at end of file +CMD sh /work/compile.sh diff --git a/vadl/test/resources/images/lcb_execution_test_rv32im/Dockerfile b/vadl/test/resources/images/lcb_execution_test_rv32im/Dockerfile index 57f499002..ac25babaa 100644 --- a/vadl/test/resources/images/lcb_execution_test_rv32im/Dockerfile +++ b/vadl/test/resources/images/lcb_execution_test_rv32im/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.7 FROM ghcr.io/openvadl/lcb-execution-test-rv32im-base-image@sha256:cad1819961bae5b736649804f1ec1892b4a91966c3bd5460b343b12dfabe03af ARG TARGET ARG SCCACHE_REDIS_ENDPOINT @@ -5,6 +6,7 @@ ARG UPSTREAM_BUILD_TARGET ARG UPSTREAM_CLANG_TARGET ARG SPIKE_TARGET ARG ABI +LABEL key=VADL_TEST_CONTAINER WORKDIR /llvm/build ENV TARGET=${TARGET} ENV UPSTREAM_CLANG_TARGET=${UPSTREAM_CLANG_TARGET} @@ -12,9 +14,10 @@ ENV LLVM_UPSTREAM_TARGETS=${UPSTREAM_BUILD_TARGET} ENV SCCACHE_REDIS_ENDPOINT=${SCCACHE_REDIS_ENDPOINT} ENV SPIKE_TARGET=${SPIKE_TARGET} ENV ABI=${ABI} +ENV SCCACHE_DIR=/root/.cache/sccache ENV PATH=/opt/riscv/bin:/opt/qemu/bin:$PATH COPY . /src WORKDIR /src -RUN make && sccache -s +RUN --mount=type=cache,target=/root/.cache/sccache make && sccache -s RUN mkdir /output -CMD sh /work/compile.sh \ No newline at end of file +CMD sh /work/compile.sh diff --git a/vadl/test/resources/images/lcb_execution_test_rv64im/Dockerfile b/vadl/test/resources/images/lcb_execution_test_rv64im/Dockerfile index 50f394be3..dd1647b75 100644 --- a/vadl/test/resources/images/lcb_execution_test_rv64im/Dockerfile +++ b/vadl/test/resources/images/lcb_execution_test_rv64im/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.7 FROM ghcr.io/openvadl/lcb-execution-test-rv64im-base-image@sha256:45115bb0a22747dd8d347051d2bcb5ee084f568c90afbdf71ef738b0849afb96 ARG TARGET ARG SCCACHE_REDIS_ENDPOINT @@ -5,6 +6,7 @@ ARG UPSTREAM_BUILD_TARGET ARG UPSTREAM_CLANG_TARGET ARG SPIKE_TARGET ARG ABI +LABEL key=VADL_TEST_CONTAINER WORKDIR /llvm/build ENV TARGET=${TARGET} ENV UPSTREAM_CLANG_TARGET=${UPSTREAM_CLANG_TARGET} @@ -12,9 +14,10 @@ ENV LLVM_UPSTREAM_TARGETS=${UPSTREAM_BUILD_TARGET} ENV SCCACHE_REDIS_ENDPOINT=${SCCACHE_REDIS_ENDPOINT} ENV SPIKE_TARGET=${SPIKE_TARGET} ENV ABI=${ABI} +ENV SCCACHE_DIR=/root/.cache/sccache ENV PATH=/opt/riscv/bin:/opt/qemu/bin:$PATH COPY . /src WORKDIR /src -RUN make && sccache -s +RUN --mount=type=cache,target=/root/.cache/sccache make && sccache -s RUN mkdir /output -CMD sh /work/compile.sh \ No newline at end of file +CMD sh /work/compile.sh diff --git a/vadl/test/resources/images/lcb_execution_test_varrisc/Dockerfile b/vadl/test/resources/images/lcb_execution_test_varrisc/Dockerfile index 663def24e..af0e8d0ea 100644 --- a/vadl/test/resources/images/lcb_execution_test_varrisc/Dockerfile +++ b/vadl/test/resources/images/lcb_execution_test_varrisc/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.7 FROM ghcr.io/openvadl/llvm19-base@sha256:743dc3df521b6721ee4a74126aff198f40b6fc585d2c06b76869cabbb3d2c015 WORKDIR /work @@ -9,6 +10,7 @@ ARG UPSTREAM_BUILD_TARGET ARG UPSTREAM_CLANG_TARGET ARG SPIKE_TARGET ARG ABI +LABEL key=VADL_TEST_CONTAINER WORKDIR /llvm/build @@ -18,7 +20,8 @@ ENV LLVM_UPSTREAM_TARGETS=${UPSTREAM_BUILD_TARGET} ENV SCCACHE_REDIS_ENDPOINT=${SCCACHE_REDIS_ENDPOINT} ENV SPIKE_TARGET=${SPIKE_TARGET} ENV ABI=${ABI} +ENV SCCACHE_DIR=/root/.cache/sccache COPY . /src WORKDIR /src -RUN make && sccache -s -RUN mkdir /output \ No newline at end of file +RUN --mount=type=cache,target=/root/.cache/sccache make && sccache -s +RUN mkdir /output diff --git a/vadl/test/resources/junit-platform.properties b/vadl/test/resources/junit-platform.properties new file mode 100644 index 000000000..b059a65dc --- /dev/null +++ b/vadl/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.extensions.autodetection.enabled=true \ No newline at end of file diff --git a/vadl/test/resources/logback-test.xml b/vadl/test/resources/logback-test.xml index 1358e8c28..f3e6f679a 100644 --- a/vadl/test/resources/logback-test.xml +++ b/vadl/test/resources/logback-test.xml @@ -9,6 +9,7 @@ + diff --git a/vadl/test/vadl/BuildkitDockerImage.java b/vadl/test/vadl/BuildkitDockerImage.java new file mode 100644 index 000000000..e435fd966 --- /dev/null +++ b/vadl/test/vadl/BuildkitDockerImage.java @@ -0,0 +1,264 @@ +// SPDX-FileCopyrightText : © 2026 TU Wien +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package vadl; + +import io.github.kper.buildkitcli.lib.BuildLog; +import io.github.kper.buildkitcli.lib.BuildOutputMode; +import io.github.kper.buildkitcli.lib.BuildProgressListener; +import io.github.kper.buildkitcli.lib.BuildResult; +import io.github.kper.buildkitcli.lib.BuildVertex; +import io.github.kper.buildkitcli.lib.BuildWarning; +import io.github.kper.buildkitcli.lib.BuildkitClient; +import io.github.kper.buildkitcli.lib.BuildkitConnectionConfig; +import io.github.kper.buildkitcli.lib.BuildkitException; +import io.github.kper.buildkitcli.lib.DockerfileBuildRequest; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.images.builder.dockerfile.DockerfileBuilder; + +/** + * A small test helper that preserves the subset of the {@code ImageFromDockerfile} API used in + * this codebase while delegating image builds to buildkit. + */ +public final class BuildkitDockerImage { + + private static final Logger logger = LoggerFactory.getLogger(BuildkitDockerImage.class); + private static final BuildkitConnectionConfig BUILDKIT_CONFIG = + BuildkitConnectionConfig.of("tcp://localhost:1234"); + + private final String imageName; + private final Map filesFromPath = new LinkedHashMap<>(); + private final Map filesFromClasspath = new LinkedHashMap<>(); + private final Map buildArgs = new LinkedHashMap<>(); + + private Path dockerfilePath; + private String dockerfileContents; + private String resolvedImageName; + + public BuildkitDockerImage() { + this("vadl-test-" + UUID.randomUUID()); + } + + public BuildkitDockerImage(String imageName) { + this.imageName = imageName; + } + + public BuildkitDockerImage withDockerfile(Path path) { + this.dockerfilePath = path.toAbsolutePath().normalize(); + this.dockerfileContents = null; + return this; + } + + public BuildkitDockerImage withDockerfileFromBuilder(Consumer consumer) { + var builder = new DockerfileBuilder(); + consumer.accept(builder); + this.dockerfileContents = builder.build(); + this.dockerfilePath = null; + return this; + } + + public BuildkitDockerImage withFileFromPath(String path, Path source) { + filesFromPath.put(normalizeContextPath(path), source.toAbsolutePath().normalize()); + return this; + } + + public BuildkitDockerImage withFileFromClasspath(String path, String resourcePath) { + filesFromClasspath.put(normalizeContextPath(path), resourcePath); + return this; + } + + public BuildkitDockerImage withBuildArg(String key, String value) { + buildArgs.put(key, value); + return this; + } + + public synchronized String getDockerImageName() { + if (resolvedImageName != null) { + return resolvedImageName; + } + + Path tempContext = null; + try { + Path contextDir; + Path dockerfile; + + if (dockerfileContents != null) { + tempContext = Files.createTempDirectory("buildkit-context-"); + stageSupplementalFiles(tempContext); + dockerfile = tempContext.resolve("Dockerfile"); + Files.writeString(dockerfile, dockerfileContents); + contextDir = tempContext; + } else if (dockerfilePath != null) { + if (filesFromPath.isEmpty() && filesFromClasspath.isEmpty()) { + dockerfile = dockerfilePath; + contextDir = dockerfilePath.getParent(); + } else { + tempContext = Files.createTempDirectory("buildkit-context-"); + FileUtils.copyDirectory(dockerfilePath.getParent().toFile(), tempContext.toFile()); + stageSupplementalFiles(tempContext); + dockerfile = tempContext.resolve(dockerfilePath.getFileName().toString()); + contextDir = tempContext; + } + } else { + throw new IllegalStateException("No Dockerfile configured for image " + imageName); + } + + buildImage(contextDir, dockerfile); + resolvedImageName = imageName; + return resolvedImageName; + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Failed to build docker image " + imageName, e); + } finally { + if (tempContext != null) { + try { + FileUtils.deleteDirectory(tempContext.toFile()); + } catch (IOException e) { + logger.warn("Could not delete temporary build context {}", tempContext, e); + } + } + } + } + + private void buildImage(Path contextDir, Path dockerfile) + throws IOException, InterruptedException { + var requestBuilder = DockerfileBuildRequest.builder(contextDir, dockerfile, imageName) + .outputMode(BuildOutputMode.DOCKER); + buildArgs.forEach(requestBuilder::buildArg); + + try (var client = new BuildkitClient(BUILDKIT_CONFIG)) { + var result = client.buildImage(requestBuilder.build(), new PrintingListener()); + loadIntoDocker(result); + } catch (BuildkitException e) { + throw new RuntimeException(e); + } + } + + private void stageSupplementalFiles(Path contextDir) throws IOException { + for (var entry : filesFromPath.entrySet()) { + copyPath(entry.getValue(), contextDir.resolve(entry.getKey())); + } + for (var entry : filesFromClasspath.entrySet()) { + copyClasspathResource(entry.getValue(), contextDir.resolve(entry.getKey())); + } + } + + private static void copyPath(Path source, Path destination) throws IOException { + if (Files.isDirectory(source)) { + FileUtils.copyDirectory(source.toFile(), destination.toFile()); + return; + } + + if (destination.getParent() != null) { + Files.createDirectories(destination.getParent()); + } + Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING); + } + + private static void copyClasspathResource(String resourcePath, Path destination) + throws IOException { + URL resource = BuildkitDockerImage.class.getResource(resourcePath); + if (resource == null) { + throw new IllegalStateException("Could not find classpath resource " + resourcePath); + } + + if ("file".equals(resource.getProtocol())) { + try { + copyPath(Path.of(resource.toURI()), destination); + return; + } catch (URISyntaxException e) { + throw new IOException("Invalid classpath resource URI for " + resourcePath, e); + } + } + + if (destination.getParent() != null) { + Files.createDirectories(destination.getParent()); + } + try (InputStream inputStream = resource.openStream()) { + Files.copy(inputStream, destination, StandardCopyOption.REPLACE_EXISTING); + } + } + + private static String normalizeContextPath(String path) { + String normalized = path.replace('\\', '/'); + while (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + while (normalized.startsWith("./")) { + normalized = normalized.substring(2); + } + if (normalized.isBlank()) { + throw new IllegalArgumentException("Context path must not be blank"); + } + return normalized; + } + + private static void loadIntoDocker(BuildResult result) throws IOException, InterruptedException { + Path exportedArchive = result.exportedArchive(); + if (exportedArchive == null) { + throw new IllegalStateException("Build result did not include a docker archive"); + } + + try { + Process process = new ProcessBuilder("docker", "load", "-i", exportedArchive.toString()) + .inheritIO() + .start(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IllegalStateException("docker load failed with exit code " + exitCode); + } + logger.info("Loading image was successful."); + } finally { + Files.deleteIfExists(exportedArchive); + } + } + + static class PrintingListener implements BuildProgressListener { + private final Logger logger = LoggerFactory.getLogger(PrintingListener.class); + + @Override + public void onVertex(BuildVertex vertex) { + if (!vertex.name().isBlank()) { + logger.info(vertex.name()); + } + } + + @Override + public void onLog(BuildLog log) { + String message = log.utf8Message(); + if (!message.isBlank()) { + logger.info(message); + } + } + + @Override + public void onWarning(BuildWarning warning) { + logger.warn("warning: {}", warning.shortMessage()); + } + } +} diff --git a/vadl/test/vadl/DockerExecutionTest.java b/vadl/test/vadl/DockerExecutionTest.java index fee88c614..21405688a 100644 --- a/vadl/test/vadl/DockerExecutionTest.java +++ b/vadl/test/vadl/DockerExecutionTest.java @@ -20,8 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.model.Mount; -import com.github.dockerjava.api.model.MountType; import com.google.errorprone.annotations.concurrent.LazyInit; import java.io.File; import java.io.FileOutputStream; @@ -35,7 +33,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; @@ -51,9 +48,7 @@ import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.images.builder.ImageFromDockerfile; import org.testcontainers.images.builder.Transferable; -import org.testcontainers.images.builder.dockerfile.DockerfileBuilder; import org.testcontainers.utility.MountableFile; import org.testcontainers.utility.ThrowingFunction; import vadl.utils.Pair; @@ -65,9 +60,6 @@ public abstract class DockerExecutionTest extends AbstractTest { @LazyInit private static Network testNetwork; - @Nullable - private static RedisCache redisCache; - @BeforeAll public static void beforeAll() { testNetwork = Network.newNetwork(); @@ -76,11 +68,6 @@ public static void beforeAll() { @AfterAll public static void afterAll() { - if (redisCache != null) { - // this call will also persist the data cached in this test run - redisCache.stop(); - } - testNetwork.close(); } @@ -121,7 +108,7 @@ protected List assertStatusCodes(String resultPath) * be copied to the host. */ protected void runContainerAndCopyDirectoryIntoContainerAndCopyOutputBack( - ImageFromDockerfile image, + BuildkitDockerImage image, List> copyMappings, String hostOutputPath, String containerResultPath) { @@ -129,6 +116,7 @@ protected void runContainerAndCopyDirectoryIntoContainerAndCopyOutputBack( containerResultPath, null); } + /** * Starts a container and checks the status code for the exited container. * It will copy the copy mappings into the container. After the container was @@ -147,7 +135,7 @@ protected void runContainerAndCopyDirectoryIntoContainerAndCopyOutputBack( * @param cmd overwrites the command which will be executed on startup. */ protected void runContainerAndCopyDirectoryIntoContainerAndCopyOutputBack( - ImageFromDockerfile image, + String image, List> copyMappings, String hostOutputPath, String containerResultPath, @@ -165,6 +153,33 @@ protected void runContainerAndCopyDirectoryIntoContainerAndCopyOutputBack( ); } + /** + * Starts a container and checks the status code for the exited container. + * It will copy the copy mappings into the container. After the container was + * executed it will copy a file back to read the result. + * It will assert that the status code is zero. If the check takes longer + * than 10 seconds or the status code is not zero then it will throw an + * exception. + * + * @param image is the docker image for the {@link GenericContainer}. + * @param copyMappings are mappings from the host to the container for the files which should + * be copied. + * @param hostOutputPath is the path where the {@code containerResultPath} should be copied + * to. + * @param containerResultPath is the path of a file which the container has computed and should + * be copied to the host. + * @param cmd overwrites the command which will be executed on startup. + */ + protected void runContainerAndCopyDirectoryIntoContainerAndCopyOutputBack( + BuildkitDockerImage image, + List> copyMappings, + String hostOutputPath, + String containerResultPath, + @Nullable String cmd) { + runContainerAndCopyDirectoryIntoContainerAndCopyOutputBack(image.getDockerImageName(), + copyMappings, hostOutputPath, containerResultPath, cmd); + } + /** * Starts a container and checks the status code for the exited container. * It will write the given {@code content} into a temporary file. The @@ -180,7 +195,7 @@ protected void runContainerAndCopyDirectoryIntoContainerAndCopyOutputBack( * @throws IOException when the temp file is writable. */ protected void runContainerAndCopyInputIntoContainer( - ImageFromDockerfile image, + BuildkitDockerImage image, String content, String containerPath) throws IOException { runContainer(image, (container) -> container @@ -204,7 +219,34 @@ protected void runContainerAndCopyInputIntoContainer( * @param cmd is the command which is executed. */ protected void runContainerAndCopyInputIntoContainer( - ImageFromDockerfile image, + BuildkitDockerImage image, + List> copyMappings, + Map environmentMappings, + String cmd) { + runContainerAndCopyInputIntoContainerAndCopyFromContainerToHost(image.getDockerImageName(), + copyMappings, + environmentMappings, + Collections.emptyList(), + cmd); + } + + + /** + * Starts a container and checks the status code for the exited container. + * It will write the given {@code content} into a temporary file. The + * temporary file requires a {@code prefix} and {@code suffix}. + * Copies the data from {@code copyMappings}. Additionally, it will + * set environment variables based on {@code environmentMappings}. + * + * @param image is the docker image for the {@link GenericContainer}. + * @param copyMappings is a list where each {@link Pair} indicates what should be copied + * from the host to the container. + * @param environmentMappings is a list where each entry defines an environment variable which + * will be set in the container. + * @param cmd is the command which is executed. + */ + protected void runContainerAndCopyInputIntoContainer( + String image, List> copyMappings, Map environmentMappings, String cmd) { @@ -232,7 +274,7 @@ protected void runContainerAndCopyInputIntoContainer( * @param cmd is the command which is executed. */ protected void runContainerAndCopyInputIntoContainerAndCopyFromContainerToHost( - ImageFromDockerfile image, + String image, List> copyMappings, Map environmentMappings, List> copyFromContainerToHost, @@ -272,7 +314,7 @@ protected void runContainerAndCopyInputIntoContainerAndCopyFromContainerToHost( * @param containerModifier a consumer that allows modification of the container configuration * @param postExecution a consumer that is called when the container successfully terminated */ - protected void runContainer(ImageFromDockerfile image, + protected void runContainer(String image, Function, GenericContainer> containerModifier, @Nullable Consumer> postExecution ) { @@ -306,23 +348,25 @@ protected void runContainer(ImageFromDockerfile image, } } - public static Network testNetwork() { - return testNetwork; - } - /** - * Returns a running redis cache. If no redis cache exists yet, it will be created. + * Starts a container and checks the status code for the exited container. + * It will assert that the status code is zero. If the check takes longer + * than 10 seconds or the status code is not zero then it will throw an + * exception. * - * @return an object containing redis cache information + * @param image is the docker image for the {@link GenericContainer}. + * @param containerModifier a consumer that allows modification of the container configuration + * @param postExecution a consumer that is called when the container successfully terminated */ - protected static synchronized RedisCache getRunningRedisCache() { - if (redisCache != null && redisCache.redisContainer.isRunning()) { - return redisCache; - } + protected void runContainer(BuildkitDockerImage image, + Function, GenericContainer> containerModifier, + @Nullable Consumer> postExecution + ) { + runContainer(image.getDockerImageName(), containerModifier, postExecution); + } - redisCache = new RedisCache("redis", testNetwork()); - redisCache.start(); - return redisCache; + public static Network testNetwork() { + return testNetwork; } /** @@ -424,111 +468,4 @@ public static T copyPathFromContainer(GenericContainer container, String throw new RuntimeException(e); } } - - - /** - * A containerized redis cache that persists the cached data across runs. - * If your test container should use the cache (e.g. via sccache) you must run the container - * (or build step) in the same network as the redis cache. - * The easiest way to do is by using the {@link RedisCache#setupEnv(ImageFromDockerfile)} - * and {@link RedisCache#setupEnv(DockerfileBuilder)}. - * While you must use the first method, the second one is only useful if you use - * the {@link DockerfileBuilder}. - */ - public record RedisCache( - String host, - int port, - GenericContainer redisContainer, - Network network - ) { - - private static final Logger log = LoggerFactory.getLogger(RedisCache.class); - - private static final String SCCACHE_REDIS_ENDPOINT = "SCCACHE_REDIS_ENDPOINT"; - - RedisCache(String hostName, Network network) { - this(hostName, 6379, constructContainer(hostName, network), network); - } - - - private void start() { - redisContainer.start(); - } - - /** - * This will stop the redis cache if it is still running. - * It will also persist the data cached during this run, so it is available in - * the next run. - */ - private void stop() { - try { - if (!redisContainer.isRunning()) { - log.info("Redis container isn't running anymore. Skipping shutdown."); - return; - } - // persist cache before shutting down - log.info("Persist redis cache data before shutdown..."); - redisContainer.execInContainer("redis-cli", "shutdown", "save"); - - redisContainer.stop(); - log.info("Stopped redis cache container."); - } catch (IOException | InterruptedException e) { - redisContainer.stop(); - throw new RuntimeException(e); - } - } - - /** - * Sets an environment variable to indicate to the distributed cache to use the redis - * docker instance as cache. - */ - public DockerfileBuilder setupEnv(DockerfileBuilder d) { - logger.info("Using redis cache: {}", redisCache); - d.env(SCCACHE_REDIS_ENDPOINT, tcpAddress()); - - // check if redis cache is available - d.run( - "timeout 5 bash -c ' modifier.withNetworkMode(network.getId())); - - return image; - } - - private String tcpAddress() { - return "tcp://" + host + ":" + port; - } - - /** - * Constructs a redis container that is mount to a cache volume. - * And exposed to the given network and the given hostName. - */ - private static GenericContainer constructContainer(String hostName, - Network network) { - return new GenericContainer<>("redis:7.4") - .withCreateContainerCmdModifier(cmd -> { - var mount = new Mount() - .withType(MountType.VOLUME) - .withSource("open-vadl-redis-cache") - .withTarget("/data"); - - Objects.requireNonNull(cmd.getHostConfig()) - .withMounts(List.of(mount)); - }) - // we need this custom network, because other containers must access - // the redis cache with the given hostname/alias - // (which is only available on custom networks) - .withNetwork(network) - .withNetworkAliases(hostName); - } - } - } diff --git a/vadl/test/vadl/RemovingDockerImagesTeardown.java b/vadl/test/vadl/RemovingDockerImagesTeardown.java new file mode 100644 index 000000000..f5ea70849 --- /dev/null +++ b/vadl/test/vadl/RemovingDockerImagesTeardown.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText : © 2026 TU Wien +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package vadl; + +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.DockerClientFactory; +import vadl.lcb.riscv.DockerRiscvImageProvider; + +public class RemovingDockerImagesTeardown + implements BeforeAllCallback, ExtensionContext.Store.CloseableResource { + private final Logger logger = LoggerFactory.getLogger(RemovingDockerImagesTeardown.class); + + @Override + public void beforeAll(ExtensionContext context) { + context.getRoot().getStore(GLOBAL) + .getOrComputeIfAbsent("teardown", key -> this); + } + + @Override + public void close() throws Throwable { + logger.info("All tests finished!"); + + var images = DockerRiscvImageProvider.images; + + if (images.isEmpty()) { + logger.info("No images to delete"); + } + + for (var imageName : images) { + logger.info("Removing image: {}", imageName); + DockerClientFactory.instance().client() + .removeImageCmd(imageName) + .withForce(true) + .exec(); + logger.info("Removing image completed: {}", imageName); + } + } +} diff --git a/vadl/test/vadl/cppCodeGen/BuiltinCTest.java b/vadl/test/vadl/cppCodeGen/BuiltinCTest.java index 757d04fbd..b5ec625e0 100644 --- a/vadl/test/vadl/cppCodeGen/BuiltinCTest.java +++ b/vadl/test/vadl/cppCodeGen/BuiltinCTest.java @@ -36,8 +36,8 @@ import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; import org.testcontainers.containers.output.OutputFrame; -import org.testcontainers.images.builder.ImageFromDockerfile; import org.testcontainers.utility.MountableFile; +import vadl.BuildkitDockerImage; import vadl.DockerExecutionTest; import vadl.cppCodeGen.common.PureFunctionCodeGenerator; import vadl.types.BuiltInTable; @@ -1803,7 +1803,7 @@ private Stream runTests(Function... functions) { try (var in = BuiltInTable.class.getResourceAsStream("/templates/common/vadl-builtins.h")) { Files.write(builtinLib, Objects.requireNonNull(in).readAllBytes()); } - var gccImage = new ImageFromDockerfile().withDockerfileFromBuilder(builder -> builder + var gccImage = new BuildkitDockerImage().withDockerfileFromBuilder(builder -> builder .from("gcc:latest")); diff --git a/vadl/test/vadl/iss/CosimInstrTest.java b/vadl/test/vadl/iss/CosimInstrTest.java index 0a5fda218..7379f15aa 100644 --- a/vadl/test/vadl/iss/CosimInstrTest.java +++ b/vadl/test/vadl/iss/CosimInstrTest.java @@ -29,8 +29,8 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DynamicTest; -import org.testcontainers.images.builder.ImageFromDockerfile; import org.testcontainers.utility.MountableFile; +import vadl.BuildkitDockerImage; public abstract class CosimInstrTest extends CosimTest { @@ -61,7 +61,7 @@ protected final Stream runTestsWith( return runQemuInstrTests(image, testCases); } - protected Stream runQemuInstrTests(ImageFromDockerfile image, + protected Stream runQemuInstrTests(BuildkitDockerImage image, Collection testCases) throws IOException { diff --git a/vadl/test/vadl/iss/CosimTest.java b/vadl/test/vadl/iss/CosimTest.java index 1f645f6b5..5bfb384a8 100644 --- a/vadl/test/vadl/iss/CosimTest.java +++ b/vadl/test/vadl/iss/CosimTest.java @@ -22,7 +22,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.images.builder.ImageFromDockerfile; +import vadl.BuildkitDockerImage; import vadl.DockerExecutionTest; import vadl.configuration.IssConfiguration; import vadl.pass.PassOrders; @@ -43,7 +43,7 @@ public abstract class CosimTest extends DockerExecutionTest { "ghcr.io/openvadl/iss-test-base@sha256:134d41337274a2f54790c13582e14a23a78617bb90554214f8a6f721c5287e85"; // specification to image cache - private static final ConcurrentHashMap issImageCache = + private static final ConcurrentHashMap issImageCache = new ConcurrentHashMap<>(); private static final Logger log = LoggerFactory.getLogger(CosimTest.class); @@ -62,7 +62,7 @@ public IssConfiguration getConfiguration(boolean doDump) { * @param specPath path to VADL specification in testSource * @return the image containing the generated QEMU ISS */ - protected ImageFromDockerfile generateIssSimulator(String specPath) { + protected BuildkitDockerImage generateIssSimulator(String specPath) { var config = IssConfiguration.from(getConfiguration(false)); return generateSimulator(issImageCache, specPath, config); } @@ -71,7 +71,7 @@ protected ImageFromDockerfile generateIssSimulator(String specPath) { * This will generate the simulator image if it is not already contained in the provided * cache. */ - private ImageFromDockerfile generateSimulator(Map cache, + private BuildkitDockerImage generateSimulator(Map cache, String specPath, IssConfiguration configuration) { return cache.computeIfAbsent(specPath, (path) -> { @@ -103,37 +103,25 @@ private ImageFromDockerfile generateSimulator(Map c * @param generatedIssSources the path to the generated ISS/QEMU sources. * @return a new image that builds the ISS at build time. */ - private ImageFromDockerfile getIssImage(Path generatedIssSources, + private BuildkitDockerImage getIssImage(Path generatedIssSources, IssConfiguration configuration ) { - // get redis cache for faster compilation using sccache - var redisCache = getRunningRedisCache(); - var targetName = configuration.targetName().toLowerCase(); var softmmuTarget = targetName + "-softmmu"; var qemuBin = "qemu-system-" + targetName; var refTarget = "," + withUpstreamTarget(); - var dockerImage = new ImageFromDockerfile() + return new BuildkitDockerImage() .withDockerfileFromBuilder(d -> { d .from(QEMU_TEST_IMAGE) .copy("iss", "/qemu"); - // use redis cache for building (sccache allows remote caching) - var cc = "sccache gcc"; - d.workDir("/qemu/build"); // configure qemu with the new target from the specification - d.run("../configure --cc='" + cc + "' --target-list=" + softmmuTarget + refTarget); - // setup redis cache endpoint environment variablef - redisCache.setupEnv(d); - // build qemu with all cpu cores and print if cache was used. - // the sccache --start-server is required, - // otherwise we get a deadlock after the last make step. - // see https://github.com/mozilla/sccache/issues/2145 - d.run("sccache --start-server && make -j$(nproc) && sccache -s"); + d.run("../configure --cc='gcc' --target-list=" + softmmuTarget + refTarget); + d.run("make -j$(nproc)"); // validate existence of generated qemu iss d.run(qemuBin + " --version"); @@ -142,9 +130,8 @@ private ImageFromDockerfile getIssImage(Path generatedIssSources, // build the cosim broker d.copy("/vadl-cosim", "/work/vadl-cosim"); d.workDir("/work/vadl-cosim"); - d.env("RUSTC_WRAPPER", "sccache"); // use --frozen to ensure that cargo does not need to download any new dependencies - d.run("sccache --start-server && cargo build --release -p vadl-cosim-broker --frozen && sccache -s"); + d.run("cargo build --release -p vadl-cosim-broker --frozen"); // add cosim broker to path d.run("mkdir -p /opt/cosim && cp ./target/release/vadl-cosim-broker /opt/cosim/"); @@ -167,9 +154,6 @@ private ImageFromDockerfile getIssImage(Path generatedIssSources, .withFileFromClasspath("/cosim_scripts", "/cosim_scripts/" + getScriptFolder()) // add vadl-cosim to image builder .withFileFromPath("/vadl-cosim", Path.of("..", "vadl-cosim")); - - // as we have to use the same network as the redis cache, we have to build it there - return redisCache.setupEnv(dockerImage); } } diff --git a/vadl/test/vadl/iss/IssInstrTest.java b/vadl/test/vadl/iss/IssInstrTest.java index bba20672e..518d28bea 100644 --- a/vadl/test/vadl/iss/IssInstrTest.java +++ b/vadl/test/vadl/iss/IssInstrTest.java @@ -30,9 +30,9 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DynamicTest; -import org.testcontainers.images.builder.ImageFromDockerfile; import org.testcontainers.shaded.com.google.common.collect.Streams; import org.testcontainers.utility.MountableFile; +import vadl.BuildkitDockerImage; public abstract class IssInstrTest extends QemuIssTest { @@ -109,7 +109,7 @@ protected final Stream runTestsWith( * @param testCases the test cases passed to the container which runs the tests * @return the test result as DynamicTests as integration with JUnit */ - protected Stream runQemuInstrTests(ImageFromDockerfile image, + protected Stream runQemuInstrTests(BuildkitDockerImage image, Collection testCases) throws IOException { diff --git a/vadl/test/vadl/iss/QemuIssTest.java b/vadl/test/vadl/iss/QemuIssTest.java index 190d5b4d9..88e836658 100644 --- a/vadl/test/vadl/iss/QemuIssTest.java +++ b/vadl/test/vadl/iss/QemuIssTest.java @@ -23,7 +23,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.images.builder.ImageFromDockerfile; +import vadl.BuildkitDockerImage; import vadl.DockerExecutionTest; import vadl.configuration.IssConfiguration; import vadl.pass.PassOrders; @@ -45,9 +45,9 @@ public abstract class QemuIssTest extends DockerExecutionTest { // specification to image cache // we must separate CAS and ISS, otherwise the CAS test would use the ISS image - private static final ConcurrentHashMap issImageCache = + private static final ConcurrentHashMap issImageCache = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap casImageCache = + private static final ConcurrentHashMap casImageCache = new ConcurrentHashMap<>(); private static final Logger log = LoggerFactory.getLogger(QemuIssTest.class); @@ -66,7 +66,7 @@ public IssConfiguration getConfiguration(boolean doDump) { * @param specPath path to VADL specification in testSource * @return the image containing the generated QEMU ISS */ - protected ImageFromDockerfile generateIssSimulator(String specPath) { + protected BuildkitDockerImage generateIssSimulator(String specPath) { var config = IssConfiguration.from(getConfiguration(false)); return generateSimulator(issImageCache, specPath, config); } @@ -75,7 +75,7 @@ protected ImageFromDockerfile generateIssSimulator(String specPath) { * This will generate the simulator image if it is not already contained in the provided * cache. */ - private ImageFromDockerfile generateSimulator(Map cache, + private BuildkitDockerImage generateSimulator(Map cache, String specPath, IssConfiguration configuration) { return cache.computeIfAbsent(specPath, (path) -> { @@ -107,38 +107,26 @@ protected List withUpstreamTargets() { * @param generatedIssSources the path to the generated ISS/QEMU sources. * @return a new image that builds the ISS at build time. */ - private ImageFromDockerfile getIssImage(Path generatedIssSources, + private BuildkitDockerImage getIssImage(Path generatedIssSources, IssConfiguration configuration ) { - // get redis cache for faster compilation using sccache - var redisCache = getRunningRedisCache(); - var targetName = configuration.targetName().toLowerCase(); var softmmuTarget = targetName + "-softmmu"; var qemuBin = "qemu-system-" + targetName; var refTargetString = String.join(",", withUpstreamTargets()); var refTarget = refTargetString.isEmpty() ? "" : "," + refTargetString; - var dockerImage = new ImageFromDockerfile() + return new BuildkitDockerImage() .withDockerfileFromBuilder(d -> { d .from(QEMU_TEST_IMAGE) .copy("iss", "/qemu"); - // use redis cache for building (sccache allows remote caching) - var cc = "sccache gcc"; - d.workDir("/qemu/build"); // configure qemu with the new target from the specification - d.run("../configure --cc='" + cc + "' --target-list=" + softmmuTarget + refTarget); - // setup redis cache endpoint environment variablef - redisCache.setupEnv(d); - // build qemu with all cpu cores and print if cache was used. - // the sccache --start-server is required, - // otherwise we get a deadlock after the last make step. - // see https://github.com/mozilla/sccache/issues/2145 - d.run("sccache --start-server && make -j$(nproc) && sccache -s"); + d.run("../configure --cc='gcc' --target-list=" + softmmuTarget + refTarget); + d.run("make -j$(nproc)"); // validate existence of generated qemu iss d.run(qemuBin + " --version"); @@ -155,9 +143,6 @@ private ImageFromDockerfile getIssImage(Path generatedIssSources, .withFileFromPath("iss", generatedIssSources) // make iss_qemu scripts available to image builder .withFileFromClasspath("/scripts", "/scripts/iss_qemu"); - - // as we have to use the same network as the redis cache, we have to build it there - return redisCache.setupEnv(dockerImage); } } diff --git a/vadl/test/vadl/lcb/AbstractPredicateCodeGeneratorCppVerificationTest.java b/vadl/test/vadl/lcb/AbstractPredicateCodeGeneratorCppVerificationTest.java index 7025865d0..aec876fc6 100644 --- a/vadl/test/vadl/lcb/AbstractPredicateCodeGeneratorCppVerificationTest.java +++ b/vadl/test/vadl/lcb/AbstractPredicateCodeGeneratorCppVerificationTest.java @@ -31,7 +31,7 @@ import net.jqwik.api.Arbitrary; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; -import org.testcontainers.images.builder.ImageFromDockerfile; +import vadl.BuildkitDockerImage; import vadl.configuration.LcbConfiguration; import vadl.cppCodeGen.common.PredicateFunctionCodeGenerator; import vadl.cppCodeGen.model.GcbCppFunctionWithBody; @@ -127,7 +127,7 @@ private static Map predicates(T return output.predicates(); } - private Pair setup(LcbConfiguration configuration, + private Pair setup(LcbConfiguration configuration, String specification) throws IOException, DuplicatedPassKeyException { @@ -147,7 +147,7 @@ private Pair setup(LcbConfiguration configuratio Path.of(configuration.outputPath() + "/encoding/")); } - return Pair.of(new ImageFromDockerfile() + return Pair.of(new BuildkitDockerImage() .withDockerfile(Paths.get(configuration.outputPath() + "/encoding/Dockerfile")), setup); } diff --git a/vadl/test/vadl/lcb/EncodingCodeGeneratorCppVerificationTest.java b/vadl/test/vadl/lcb/EncodingCodeGeneratorCppVerificationTest.java index 6e7cf45b0..cec334409 100644 --- a/vadl/test/vadl/lcb/EncodingCodeGeneratorCppVerificationTest.java +++ b/vadl/test/vadl/lcb/EncodingCodeGeneratorCppVerificationTest.java @@ -27,7 +27,7 @@ import net.jqwik.api.Arbitrary; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; -import org.testcontainers.images.builder.ImageFromDockerfile; +import vadl.BuildkitDockerImage; import vadl.configuration.LcbConfiguration; import vadl.cppCodeGen.CppTypeMap; import vadl.cppCodeGen.common.GcbAccessFunctionCodeGenerator; @@ -64,7 +64,7 @@ Collection instructions() throws IOException, DuplicatedPassKeyExce Path.of(configuration.outputPath() + "/encoding/")); } - var image = new ImageFromDockerfile() + var image = new BuildkitDockerImage() .withDockerfile(Paths.get(configuration.outputPath() + "/encoding/Dockerfile")); // Generate files and output them into the temporary directory. @@ -72,7 +72,7 @@ Collection instructions() throws IOException, DuplicatedPassKeyExce } private Collection generateInputs(TestSetup setup, - ImageFromDockerfile image, + BuildkitDockerImage image, Path path) throws IOException { var passManager = setup.passManager(); diff --git a/vadl/test/vadl/lcb/LcbDockerExecutionTest.java b/vadl/test/vadl/lcb/LcbDockerExecutionTest.java index 3045dfcda..8f47cdce5 100644 --- a/vadl/test/vadl/lcb/LcbDockerExecutionTest.java +++ b/vadl/test/vadl/lcb/LcbDockerExecutionTest.java @@ -33,10 +33,6 @@ import vadl.utils.Pair; public abstract class LcbDockerExecutionTest extends AbstractLcbTest { - protected boolean enableDebug() { - return false; - } - protected abstract String getTarget(); protected abstract String getUpstreamBuildTarget(); @@ -96,23 +92,20 @@ protected void run(String specPath, String cmd) throws DuplicatedPassKeyExceptio protected void run(String specPath, String cmd, Map environments) throws DuplicatedPassKeyException, IOException { - var doDebug = enableDebug(); var configuration = getConfiguration(); runLcb(configuration, specPath); copyIntoDockerContext(configuration); - var redisCache = getRunningRedisCache(); var cachedImage = - DockerRiscvImageProvider.image(redisCache, + DockerRiscvImageProvider.image( configuration.outputPath() + "/lcb/Dockerfile", getImageName(), getTarget(), getUpstreamBuildTarget(), getUpstreamClangTarget(), getSpikeTarget(), - getAbi(), - doDebug); + getAbi()); runContainerAndCopyInputIntoContainer(cachedImage, List.of(Pair.of(Path.of("../../open-vadl/vadl-test/main/resources/llvm/riscv/spike"), @@ -121,4 +114,3 @@ protected void run(String specPath, String cmd, Map environments cmd); } } - diff --git a/vadl/test/vadl/lcb/LcbDockerInputFileExecutionTest.java b/vadl/test/vadl/lcb/LcbDockerInputFileExecutionTest.java index 4139270dc..b4bb86cd2 100644 --- a/vadl/test/vadl/lcb/LcbDockerInputFileExecutionTest.java +++ b/vadl/test/vadl/lcb/LcbDockerInputFileExecutionTest.java @@ -63,23 +63,20 @@ protected List runEach(String specPath, List sourceDirector String cmd) throws DuplicatedPassKeyException, IOException { - var doDebug = false; var configuration = getConfiguration(); runLcb(configuration, specPath); copyIntoDockerContext(configuration); - var redisCache = getRunningRedisCache(); var cachedImage = - DockerRiscvImageProvider.image(redisCache, + DockerRiscvImageProvider.image( configuration.outputPath() + "/lcb/Dockerfile", getImageName(), getTarget(), getUpstreamBuildTarget(), getUpstreamClangTarget(), getSpikeTarget(), - getAbi(), - doDebug); + getAbi()); return sourceDirectories.stream() .map(sourceDirectory -> Pair.of(sourceDirectory, inputFiles(sourceDirectory))).flatMap( diff --git a/vadl/test/vadl/lcb/riscv/AbstractLcbBenchmarkTest.java b/vadl/test/vadl/lcb/riscv/AbstractLcbBenchmarkTest.java index 02c3a2662..97fa0ea63 100644 --- a/vadl/test/vadl/lcb/riscv/AbstractLcbBenchmarkTest.java +++ b/vadl/test/vadl/lcb/riscv/AbstractLcbBenchmarkTest.java @@ -28,7 +28,6 @@ public abstract class AbstractLcbBenchmarkTest extends LcbDockerExecutionTest { @Override protected void run(String specPath, String cmd, Map environments) throws DuplicatedPassKeyException, IOException { - var doDebug = enableDebug(); var hostPath = System.getenv("EMBENCH_BENCHMARK_RESULT_HOST_PATH"); var guestPath = System.getenv("EMBENCH_BENCHMARK_RESULT_GUEST_PATH"); @@ -39,17 +38,15 @@ protected void run(String specPath, String cmd, Map environments runLcb(configuration, specPath); copyIntoDockerContext(configuration); - var redisCache = getRunningRedisCache(); var cachedImage = - DockerRiscvImageProvider.image(redisCache, + DockerRiscvImageProvider.image( configuration.outputPath() + "/lcb/Dockerfile", getTarget(), getImageName(), getUpstreamBuildTarget(), getUpstreamClangTarget(), getSpikeTarget(), - getAbi(), - doDebug); + getAbi()); runContainerAndCopyDirectoryIntoContainerAndCopyOutputBack(cachedImage, List.of(), diff --git a/vadl/test/vadl/lcb/riscv/DockerRiscvImageProvider.java b/vadl/test/vadl/lcb/riscv/DockerRiscvImageProvider.java index 7f6b1fa2e..7378defd0 100644 --- a/vadl/test/vadl/lcb/riscv/DockerRiscvImageProvider.java +++ b/vadl/test/vadl/lcb/riscv/DockerRiscvImageProvider.java @@ -16,23 +16,59 @@ package vadl.lcb.riscv; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import org.testcontainers.images.builder.ImageFromDockerfile; -import vadl.DockerExecutionTest; +import io.github.kper.buildkitcli.lib.BuildLog; +import io.github.kper.buildkitcli.lib.BuildOutputMode; +import io.github.kper.buildkitcli.lib.BuildProgressListener; +import io.github.kper.buildkitcli.lib.BuildResult; +import io.github.kper.buildkitcli.lib.BuildVertex; +import io.github.kper.buildkitcli.lib.BuildWarning; +import io.github.kper.buildkitcli.lib.BuildkitClient; +import io.github.kper.buildkitcli.lib.BuildkitConnectionConfig; +import io.github.kper.buildkitcli.lib.DockerfileBuildRequest; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.HashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * A singleton implementation to keep the reference to a {@link ImageFromDockerfile} to avoid + * A singleton implementation to keep the reference to the generated docker image to avoid * recompilation between {@link LlvmRiscvAssemblyTest} and {@link SpikeRiscvSimulationTest}. */ public class DockerRiscvImageProvider { - private static Map images = new HashMap<>(); + private static final Logger logger = LoggerFactory.getLogger(DockerRiscvImageProvider.class); + public static HashSet images = new HashSet<>(); + + static class PrintingListener implements BuildProgressListener { + private final Logger logger = LoggerFactory.getLogger(PrintingListener.class); + + @Override + public void onVertex(BuildVertex vertex) { + if (!vertex.name().isBlank()) { + logger.info(vertex.name()); + } + } + + @Override + public void onLog(BuildLog log) { + String message = log.utf8Message(); + if (!message.isBlank()) { + logger.info(message); + } + } + + @Override + public void onWarning(BuildWarning warning) { + logger.warn("warning: {}", warning.shortMessage()); + } + } + /** - * Create an {@link ImageFromDockerfile} or return an already existing image. + * Create a docker image or return an already existing image. * - * @param redisCache is the cache for building LLVM. * @param pathDockerFile is the path to the dockerfile which should be built. * @param imageName is the name of the generated and cached docker image * @param target is the name of the processor. @@ -41,40 +77,68 @@ public class DockerRiscvImageProvider { * compiler. * @param spikeTarget is the ISA for spike to run the executable. * @param abi which should be chosen for the gcc linker. - * @param doDebug if the flag is {@code true} then the image will not be deleted. + * @return the name of the image. * @throws RuntimeException when the {@code isCI} environment variable and {@code doDebug} are * activated. */ - public static ImageFromDockerfile image(DockerExecutionTest.RedisCache redisCache, - String pathDockerFile, - String imageName, - String target, - String upstreamBuildTarget, - String upstreamClangTarget, - String spikeTarget, - String abi, - boolean doDebug) { - var image = images.get(imageName); - if (image == null) { + public static String image(String pathDockerFile, + String imageName, + String target, + String upstreamBuildTarget, + String upstreamClangTarget, + String spikeTarget, + String abi) throws IOException { + var imageKey = "tc_spike_riscv_" + imageName; + var image = images.contains(imageKey); + if (!image) { - var deleteOnExit = !doDebug; + try (var client = new BuildkitClient( + BuildkitConnectionConfig.of("tcp://localhost:1234").withTimeout( + Duration.ofHours(2)))) { + Path dockerfile = Path.of(pathDockerFile); + var requestBuilder = DockerfileBuildRequest.builder( + dockerfile.getParent(), + dockerfile, + imageKey + ) + .buildArg("TARGET", target) + .buildArg("UPSTREAM_BUILD_TARGET", upstreamBuildTarget) + .buildArg("UPSTREAM_CLANG_TARGET", upstreamClangTarget) + .buildArg("ABI", abi) + .buildArg("SPIKE_TARGET", spikeTarget) + .outputMode(BuildOutputMode.DOCKER); - if ("true".equals(System.getenv("isCI")) && !deleteOnExit) { - throw new RuntimeException("It is not allowed to activate 'deleteOnExit' in the CI"); - } + BuildResult result = client.buildImage(requestBuilder.build(), new PrintingListener()); + logger.info("Image was built: {}", imageKey); + logger.info("Loading image: {}", imageKey); + loadIntoDocker(result); - var img = redisCache.setupEnv(new ImageFromDockerfile("tc_spike_riscv_" - + imageName, deleteOnExit) - .withDockerfile(Paths.get(pathDockerFile)) - .withBuildArg("TARGET", target) - .withBuildArg("UPSTREAM_BUILD_TARGET", upstreamBuildTarget) - .withBuildArg("UPSTREAM_CLANG_TARGET", upstreamClangTarget) - .withBuildArg("ABI", abi) - .withBuildArg("SPIKE_TARGET", spikeTarget)); - images.put(imageName, img); - return img; + images.add(imageKey); + return imageKey; + } catch (Exception e) { + throw new RuntimeException(e); + } } else { - return image; + logger.info("Image was already in the cache: {}", imageKey); + return imageKey; + } + } + + private static void loadIntoDocker(BuildResult result) throws Exception { + Path exportedArchive = result.exportedArchive(); + if (exportedArchive == null) { + throw new IllegalStateException("Build result did not include a docker archive"); + } + try { + Process process = new ProcessBuilder("docker", "load", "-i", exportedArchive.toString()) + .inheritIO() + .start(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IllegalStateException("docker load failed with exit code " + exitCode); + } + } finally { + Files.deleteIfExists(exportedArchive); } } } diff --git a/vadl/test/vadl/lcb/riscv/riscv64/verification/ImmediateExtractionCodeGeneratorCppVerificationRiscv64Test.java b/vadl/test/vadl/lcb/riscv/riscv64/verification/ImmediateExtractionCodeGeneratorCppVerificationRiscv64Test.java index 9e26b2361..9224fc561 100644 --- a/vadl/test/vadl/lcb/riscv/riscv64/verification/ImmediateExtractionCodeGeneratorCppVerificationRiscv64Test.java +++ b/vadl/test/vadl/lcb/riscv/riscv64/verification/ImmediateExtractionCodeGeneratorCppVerificationRiscv64Test.java @@ -29,8 +29,8 @@ import net.jqwik.api.Arbitrary; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; -import org.testcontainers.images.builder.ImageFromDockerfile; import org.testcontainers.shaded.com.google.common.collect.Streams; +import vadl.BuildkitDockerImage; import vadl.cppCodeGen.common.GcbAccessFunctionCodeGenerator; import vadl.cppCodeGen.model.GcbImmediateExtractionCppFunction; import vadl.lcb.AbstractLcbTest; @@ -65,7 +65,7 @@ Collection instructions() throws IOException, DuplicatedPassKeyExce Path.of(configuration.outputPath() + "/encoding/")); } - var image = new ImageFromDockerfile() + var image = new BuildkitDockerImage() .withDockerfile(Paths.get(configuration.outputPath() + "/encoding/Dockerfile")); return generateInputs(testSetup, image, configuration.outputPath()); @@ -79,7 +79,7 @@ Collection instructions() throws IOException, DuplicatedPassKeyExce * done in the container test with `set_bits` function. */ private Collection generateInputs(TestSetup setup, - ImageFromDockerfile image, + BuildkitDockerImage image, Path path) throws IOException { List> copyMappings = new ArrayList<>(); setup.specification() diff --git a/vadl/test/vadl/lcb/riscv/riscv64/verification/RelocationCodeGeneratorCppVerificationRiscv64Test.java b/vadl/test/vadl/lcb/riscv/riscv64/verification/RelocationCodeGeneratorCppVerificationRiscv64Test.java index 6dce457c6..3bf48a705 100644 --- a/vadl/test/vadl/lcb/riscv/riscv64/verification/RelocationCodeGeneratorCppVerificationRiscv64Test.java +++ b/vadl/test/vadl/lcb/riscv/riscv64/verification/RelocationCodeGeneratorCppVerificationRiscv64Test.java @@ -27,7 +27,7 @@ import net.jqwik.api.Arbitrary; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; -import org.testcontainers.images.builder.ImageFromDockerfile; +import vadl.BuildkitDockerImage; import vadl.cppCodeGen.CppTypeMap; import vadl.cppCodeGen.common.UpdateFieldRelocationFunctionCodeGenerator; import vadl.cppCodeGen.common.ValueRelocationFunctionCodeGenerator; @@ -74,14 +74,14 @@ Collection instructions() throws IOException, DuplicatedPassKeyExce Path.of(configuration.outputPath() + "/encoding/")); } - var image = new ImageFromDockerfile() + var image = new BuildkitDockerImage() .withDockerfile(Paths.get(configuration.outputPath() + "/encoding/Dockerfile")); return generateInputs(testSetup, image, configuration.outputPath(), testSetup.specification()); } private Collection generateInputs(TestSetup testSetup, - ImageFromDockerfile image, + BuildkitDockerImage image, Path path, Specification specification) throws IOException { var output = (GenerateLinkerComponentsPass.Output) testSetup.passManager().getPassResults() diff --git a/vadl/test/vadl/rtl/RtlDockerTest.java b/vadl/test/vadl/rtl/RtlDockerTest.java index 04db2b2ab..95cf0d693 100644 --- a/vadl/test/vadl/rtl/RtlDockerTest.java +++ b/vadl/test/vadl/rtl/RtlDockerTest.java @@ -22,7 +22,7 @@ import java.nio.file.Path; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.testcontainers.images.builder.ImageFromDockerfile; +import vadl.BuildkitDockerImage; import vadl.DockerExecutionTest; import vadl.configuration.RtlConfiguration; import vadl.pass.PassOrders; @@ -30,10 +30,10 @@ public abstract class RtlDockerTest extends DockerExecutionTest { - private static final ConcurrentHashMap imageCache = + private static final ConcurrentHashMap imageCache = new ConcurrentHashMap<>(); - protected ImageFromDockerfile generateRtlImage(String specPath, + protected BuildkitDockerImage generateRtlImage(String specPath, RtlConfiguration configuration) { final var cacheKey = String.valueOf(Set.of(specPath, configuration).hashCode()); @@ -51,7 +51,7 @@ protected ImageFromDockerfile generateRtlImage(String specPath, }); } - private ImageFromDockerfile getImage(RtlConfiguration configuration + private BuildkitDockerImage getImage(RtlConfiguration configuration ) { // find output dir @@ -60,10 +60,7 @@ private ImageFromDockerfile getImage(RtlConfiguration configuration throw new IllegalStateException("RTL output path was not found (not generated?)"); } - // Get redis cache for faster compilation using sccache - var redisCache = getRunningRedisCache(); - - var dockerImage = new ImageFromDockerfile() + return new BuildkitDockerImage() .withDockerfileFromBuilder(d -> { d.from(RTL_BASE_IMAGE); @@ -82,9 +79,6 @@ private ImageFromDockerfile getImage(RtlConfiguration configuration .withFileFromPath("/rtl", outputPath) // make scripts available to image builder .withFileFromClasspath("/scripts", "/scripts/rtl"); - - // As we have to use the same network as the redis cache, we have to build it there - return redisCache.setupEnv(dockerImage); } } diff --git a/vadl/test/vadl/viam/algebraic_simplification/AlgebraicSimplificationTest.java b/vadl/test/vadl/viam/algebraic_simplification/AlgebraicSimplificationTest.java index aa5681205..ce99f0f6e 100644 --- a/vadl/test/vadl/viam/algebraic_simplification/AlgebraicSimplificationTest.java +++ b/vadl/test/vadl/viam/algebraic_simplification/AlgebraicSimplificationTest.java @@ -29,7 +29,7 @@ import org.junit.jupiter.api.parallel.ExecutionMode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.images.builder.ImageFromDockerfile; +import vadl.BuildkitDockerImage; import vadl.DockerExecutionTest; import vadl.pass.PassResults; import vadl.viam.Instruction; @@ -47,7 +47,7 @@ public class AlgebraicSimplificationTest extends DockerExecutionTest { private static final String MOUNT_PATH = "/app/main.py"; - private static final ImageFromDockerfile DOCKER_IMAGE = new ImageFromDockerfile() + private static final BuildkitDockerImage DOCKER_IMAGE = new BuildkitDockerImage() .withDockerfileFromBuilder(builder -> builder .from("python:3.8")