Skip to content

Commit

Permalink
Merge pull request #2198 from lf-lang/docker-build
Browse files Browse the repository at this point in the history
More user-friendly Docker support
  • Loading branch information
lhstrh authored Mar 7, 2024
2 parents ce73769 + 8b5e98a commit 6759260
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 100 deletions.
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
114 changes: 50 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,57 @@ 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);
var success = false;
try {
success = cCompiler.runCCompiler(this, context);
} catch (IOException e) {
messageReporter.nowhere().error("Unexpected error during compilation.");
} finally {
if (!success) {
// If compilation failed, remove any bin files that may have been created.
messageReporter.nowhere().error("Compilation was unsuccessful.");
CUtil.deleteBinFiles(fileConfig);
context.unsuccessfulFinish();
} else {
try {
cleanCode.writeToFile(targetFile);
} catch (IOException e) {
messageReporter
.nowhere()
.warning("Generated code may still contain line directives.");
}
context.finish(GeneratorResult.Status.COMPILED, null);
}
}
}
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/bash
set -euo pipefail
cd $(dirname "$0")
cd ..
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,8 @@ protected String generateDockerFileContent() {
COPY include /reactor-c/include
WORKDIR /reactor-c/core/federated/RTI
%s
RUN mkdir build && \\
RUN rm -rf build && \\
mkdir build && \\
cd build && \\
cmake ../ && \\
make && \\
Expand Down

0 comments on commit 6759260

Please sign in to comment.