Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More user-friendly Docker support #2198

Merged
merged 10 commits into from
Mar 7, 2024
42 changes: 30 additions & 12 deletions core/src/main/java/org/lflang/federated/generator/FedGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,13 @@ public boolean doGenerate(Resource resource, LFGeneratorContext context) throws
// If the RTI is to be built locally, set up a build environment for it.
prepareRtiBuildEnvironment(context);

var useDocker = context.getTargetConfig().get(DockerProperty.INSTANCE).enabled();

Map<Path, CodeMap> codeMapMap =
compileFederates(
context,
lf2lfCodeMapMap,
subContexts -> {
createDockerFiles(context, subContexts);
generateLaunchScript();
// If an error has occurred during codegen of any federate, report it.
subContexts.forEach(
c -> {
Expand All @@ -195,12 +195,35 @@ public boolean doGenerate(Resource resource, LFGeneratorContext context) throws
.error("Failure during code generation of " + c.getFileConfig().srcFile);
}
});
if (useDocker) {
buildUsingDocker(context, subContexts);
} else {
generateLaunchScript();
}
});

context.finish(Status.COMPILED, codeMapMap);
return false;
}

/**
* Create Dockerfiles and docker-compose.yml, build, and create a launcher.
*
* @param context The main generator context.
* @param subContexts The context for the federates.
*/
private void buildUsingDocker(LFGeneratorContext context, List<SubContext> subContexts) {
try {
var dockerGen = new FedDockerComposeGenerator(context, rtiConfig.getHost());
dockerGen.writeDockerComposeFile(createDockerFiles(context, subContexts));
if (dockerGen.build()) {
dockerGen.createLauncher();
}
} catch (IOException e) {
context.getErrorReporter().nowhere().error("Unsuccessful Docker build.");
}
}

/**
* Prepare a build environment for the rti alongside the generated sources of the federates.
*
Expand Down Expand Up @@ -235,12 +258,12 @@ private void generateLaunchScript() {
* @param context The main context in which the federation has been compiled.
* @param subContexts The subcontexts in which the federates have been compiled.
*/
private void createDockerFiles(LFGeneratorContext context, List<SubContext> subContexts) {
if (!context.getTargetConfig().get(DockerProperty.INSTANCE).enabled()) return;
private List<DockerData> createDockerFiles(
LFGeneratorContext context, List<SubContext> subContexts) {
final List<DockerData> services = new ArrayList<>();
// 1. create a Dockerfile for each federate
for (SubContext subContext : subContexts) { // Inherit Docker options from main context

for (SubContext subContext : subContexts) {
// Inherit Docker options from main context
DockerProperty.INSTANCE.override(
subContext.getTargetConfig(), context.getTargetConfig().get(DockerProperty.INSTANCE));
var dockerGenerator = dockerGeneratorFactory(subContext);
Expand All @@ -252,12 +275,7 @@ private void createDockerFiles(LFGeneratorContext context, List<SubContext> subC
}
services.add(dockerData);
}
// 2. create a docker-compose.yml for the federation
try {
new FedDockerComposeGenerator(context, rtiConfig.getHost()).writeDockerComposeFile(services);
} catch (IOException e) {
throw new RuntimeIOException(e);
}
return services;
}

/**
Expand Down
104 changes: 40 additions & 64 deletions core/src/main/java/org/lflang/generator/c/CGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
import java.util.stream.Stream;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.xbase.lib.Exceptions;
import org.eclipse.xtext.xbase.lib.IterableExtensions;
import org.lflang.FileConfig;
import org.lflang.ast.ASTUtils;
import org.lflang.ast.DelayedConnectionTransformation;
Expand Down Expand Up @@ -445,17 +444,6 @@ public void doGenerate(Resource resource, LFGeneratorContext context) {
CompileDefinitionsProperty.INSTANCE.update(
targetConfig, Map.of("NUMBER_OF_WATCHDOGS", String.valueOf(nWatchdogs)));

// Create docker file.
if (targetConfig.get(DockerProperty.INSTANCE).enabled() && mainDef != null) {
try {
var dockerData = getDockerGenerator(context).generateDockerData();
dockerData.writeDockerFile();
(new DockerComposeGenerator(context)).writeDockerComposeFile(List.of(dockerData));
} catch (IOException e) {
throw new RuntimeException("Error while writing Docker files", e);
}
}

var isArduino =
targetConfig.getOrDefault(PlatformProperty.INSTANCE).platform() == Platform.ARDUINO;

Expand Down Expand Up @@ -519,54 +507,13 @@ public void doGenerate(Resource resource, LFGeneratorContext context) {
return;
}

// If this code generator is directly compiling the code, compile it now so that we
// clean it up after, removing the #line directives after errors have been reported.
if (!targetConfig.get(NoCompileProperty.INSTANCE)
&& !targetConfig.get(DockerProperty.INSTANCE).enabled()
&& IterableExtensions.isNullOrEmpty(targetConfig.get(BuildCommandsProperty.INSTANCE))
// This code is unreachable in LSP_FAST mode, so that check is omitted.
&& context.getMode() != LFGeneratorContext.Mode.LSP_MEDIUM) {
// FIXME: Currently, a lack of main is treated as a request to not produce
// a binary and produce a .o file instead. There should be a way to control
// this.
// Create an anonymous Runnable class and add it to the compileThreadPool
// so that compilation can happen in parallel.
var cleanCode = code.removeLines("#line");

var execName = lfModuleName;
var threadFileConfig = fileConfig;
var generator =
this; // FIXME: currently only passed to report errors with line numbers in the Eclipse
// IDE
var CppMode = cppMode;
// generatingContext.reportProgress(
// String.format("Generated code for %d/%d executables. Compiling...", federateCount,
// federates.size()),
// 100 * federateCount / federates.size()
// ); // FIXME: Move to FedGenerator
// Create the compiler to be used later

var cCompiler = new CCompiler(targetConfig, threadFileConfig, messageReporter, CppMode);
try {
if (!cCompiler.runCCompiler(generator, context)) {
// If compilation failed, remove any bin files that may have been created.
CUtil.deleteBinFiles(threadFileConfig);
// If finish has already been called, it is illegal and makes no sense. However,
// if finish has already been called, then this must be a federated execution.
context.unsuccessfulFinish();
} else {
context.finish(GeneratorResult.Status.COMPILED, null);
}
cleanCode.writeToFile(targetFile);
} catch (IOException e) {
Exceptions.sneakyThrow(e);
}
}
var customBuildCommands = targetConfig.get(BuildCommandsProperty.INSTANCE);
var dockerBuild = targetConfig.get(DockerProperty.INSTANCE);

// If a build directive has been given, invoke it now.
// Note that the code does not get cleaned in this case.
if (!targetConfig.get(NoCompileProperty.INSTANCE)) {
if (!IterableExtensions.isNullOrEmpty(targetConfig.get(BuildCommandsProperty.INSTANCE))) {
if (targetConfig.get(NoCompileProperty.INSTANCE)) {
context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(context, null));
} else if (context.getMode() != LFGeneratorContext.Mode.LSP_MEDIUM) {
if (customBuildCommands != null && !customBuildCommands.isEmpty()) {
CUtil.runBuildCommand(
fileConfig,
targetConfig,
Expand All @@ -575,18 +522,47 @@ public void doGenerate(Resource resource, LFGeneratorContext context) {
this::reportCommandErrors,
context.getMode());
context.finish(GeneratorResult.Status.COMPILED, null);
} else if (dockerBuild.enabled()) {
buildUsingDocker();
} else {
var cleanCode = code.removeLines("#line");
var cCompiler = new CCompiler(targetConfig, fileConfig, messageReporter, cppMode);
try {
if (!cCompiler.runCCompiler(this, context)) {
// If compilation failed, remove any bin files that may have been created.
CUtil.deleteBinFiles(fileConfig);
context.unsuccessfulFinish();
} else {
context.finish(GeneratorResult.Status.COMPILED, null);
}
cleanCode.writeToFile(targetFile);
} catch (IOException e) {
messageReporter.nowhere().warning("Generated code may still contain line directives.");
}
petervdonovan marked this conversation as resolved.
Show resolved Hide resolved
}
if (!errorsOccurred()) {
messageReporter.nowhere().info("Compiled binary is in " + fileConfig.binPath);
}
} else {
context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(context, null));
}

// In case we are in Eclipse, make sure the generated code is visible.
GeneratorUtils.refreshProject(resource, context.getMode());
}

/** Create Dockerfiles and docker-compose.yml, build, and create a launcher. */
private void buildUsingDocker() {
// Create docker file.
var dockerCompose = new DockerComposeGenerator(context);
var dockerData = getDockerGenerator(context).generateDockerData();
try {
dockerData.writeDockerFile();
dockerCompose.writeDockerComposeFile(List.of(dockerData));
} catch (IOException e) {
throw new RuntimeException("Error while writing Docker files", e);
}
var success = dockerCompose.build();
if (success && mainDef != null) {
dockerCompose.createLauncher();
}
}

private void generateCodeFor(String lfModuleName) throws IOException {
code.pr(generateDirectives());
code.pr(new CMainFunctionGenerator(targetConfig).generateCode());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package org.lflang.generator.docker;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.lflang.generator.LFGeneratorContext;
import org.lflang.util.FileUtil;
import org.lflang.util.LFCommand;

/**
* Code generator for docker-compose configurations.
Expand All @@ -15,14 +18,10 @@
*/
public class DockerComposeGenerator {

/** Path to the docker-compose.yml file. */
protected final Path path;

/** Context of the code generator. */
protected final LFGeneratorContext context;

public DockerComposeGenerator(LFGeneratorContext context) {
this.path = context.getFileConfig().getSrcGenPath().resolve("docker-compose.yml");
this.context = context;
}

Expand Down Expand Up @@ -52,22 +51,7 @@ protected String generateDockerServices(List<DockerData> services) {
%s
"""
.formatted(
services.stream()
.map(data -> getServiceDescription(data))
.collect(Collectors.joining("\n")));
}

/** Return the command to build and run using the docker-compose configuration. */
public String getUsageInstructions() {
return """
#####################################
To build and run:
pushd %s && docker compose up --build
To return to the current working directory afterwards:
popd
#####################################
"""
.formatted(path.getParent());
services.stream().map(this::getServiceDescription).collect(Collectors.joining("\n")));
}

/** Turn given docker data into a string. */
Expand Down Expand Up @@ -116,7 +100,53 @@ public void writeDockerComposeFile(List<DockerData> services, String networkName
var contents =
String.join(
"\n", this.generateDockerServices(services), this.generateDockerNetwork(networkName));
FileUtil.writeToFile(contents, path);
context.getErrorReporter().nowhere().info(getUsageInstructions());
FileUtil.writeToFile(
contents, context.getFileConfig().getSrcGenPath().resolve("docker-compose.yml"));
}

/**
* Build using docker compose.
*
* @return {@code true} if successful,{@code false} otherwise.
*/
public boolean build() {
return Objects.requireNonNull(
LFCommand.get(
"docker",
List.of("compose", "build"),
false,
context.getFileConfig().getSrcGenPath()))
.run()
== 0;
}

/** Create a launcher script that invokes Docker. */
public void createLauncher() {
var fileConfig = context.getFileConfig();
var packageRoot = fileConfig.srcPkgPath;
var srcGenPath = fileConfig.getSrcGenPath();
var file = fileConfig.binPath.resolve(fileConfig.name).toFile();
var script =
"""
#!/bin/sh
lhstrh marked this conversation as resolved.
Show resolved Hide resolved
set -euo pipefail
cd $(dirname "$0")
cd ..
lhstrh marked this conversation as resolved.
Show resolved Hide resolved
cd "%s"
docker compose up
"""
.formatted(packageRoot.relativize(srcGenPath));
var messageReporter = context.getErrorReporter();
try {
var writer = new BufferedWriter(new FileWriter(file));
writer.write(script);
writer.close();
} catch (IOException e) {
messageReporter.nowhere().warning("Unable to write launcher to: " + file.getAbsolutePath());
}

if (!file.setExecutable(true, false)) {
messageReporter.nowhere().warning("Unable to make launcher script executable.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ protected String generateDockerFileContent() {
COPY include /reactor-c/include
WORKDIR /reactor-c/core/federated/RTI
%s
RUN mkdir build && \\
RUN mkdir -p build && \\
petervdonovan marked this conversation as resolved.
Show resolved Hide resolved
cd build && \\
cmake ../ && \\
make && \\
Expand Down
Loading