diff --git a/example/C/src/DistributedHelloWorld/docker/HelloWorldContainerized.lf b/example/C/src/DistributedHelloWorld/docker/HelloWorldContainerized.lf new file mode 100644 index 0000000000..70161f6946 --- /dev/null +++ b/example/C/src/DistributedHelloWorld/docker/HelloWorldContainerized.lf @@ -0,0 +1,22 @@ +/** + * Containerized distributed LF program where a MessageGenerator creates a string + * message that is sent via the RTI (runtime infrastructure) to a + * receiver that prints the message. + * + * For run instructions, see README. + * + * @author Edward A. Lee + */ +target C { + timeout: 10 secs, + docker: true +}; + +import MessageGenerator from "../HelloWorld.lf" +import PrintMessage from "../HelloWorld.lf" + +federated reactor HelloWorldContainerized at rti { + source = new MessageGenerator(prefix = "Hello World"); + print = new PrintMessage(); + source.message -> print.message; +} \ No newline at end of file diff --git a/example/C/src/DistributedHelloWorld/docker/README.md b/example/C/src/DistributedHelloWorld/docker/README.md new file mode 100644 index 0000000000..0d9bb582ca --- /dev/null +++ b/example/C/src/DistributedHelloWorld/docker/README.md @@ -0,0 +1,51 @@ +# ContainerizedFederatedHelloWorld +This is a basic example showing containerized federated execution. + +For more details, see: [Containerized Execution in Lingua Franca](https://github.com/lf-lang/lingua-franca/wiki/Containerized-Execution) + +## Run instructions +Put HelloWorld.lf in a src directory. +Then, run: +```bash +lfc HelloWorldContainerized.lf +``` + +There would be 3 build messages, 1 for the RTI and 2 for the reactors, indicating where the docker file is generated, as well as the instruction to build the docker file. + +A message would look something like this: +``` +Dockerfile for <something> written to <some_directory>/<name_of_dockerfile> +##################################### +To build the docker image, use: + + docker build -t <some_name> -f <some_directory>/<name_of_dockerfile> <some_path> + +##################################### +``` + +If you cannot find the build message for the RTI, try a clean build using: +```bash +lfc --clean HelloWorld.lf +``` + +Then, use the printed commands to build the 3 docker images. + +Set up a docker network using +```bash +docker network create lf +``` + +Open 3 terminals, 1 for the RTI and 1 for each reactor. + +Run the RTI: +```bash +docker run --rm -it --network=lf --name=rti rti -i 1 -n 2 +``` + +Run the two reactors: +```bash +docker run --rm -it --network=lf helloworldcontainerized_source -i 1 +``` +```bash +docker run --rm -it --network=lf helloworldcontainerized_print -i 1 +``` \ No newline at end of file diff --git a/org.lflang/src/org/lflang/TargetProperty.java b/org.lflang/src/org/lflang/TargetProperty.java index 311c67ff53..38222d8f17 100644 --- a/org.lflang/src/org/lflang/TargetProperty.java +++ b/org.lflang/src/org/lflang/TargetProperty.java @@ -167,7 +167,7 @@ public enum TargetProperty { * true or false, or a dictionary of options. */ DOCKER("docker", UnionType.DOCKER_UNION, - Arrays.asList(Target.C, Target.CCPP), (config, value, err) -> { + Arrays.asList(Target.C, Target.CCPP, Target.Python), (config, value, err) -> { if (value.getLiteral() != null) { if (ASTUtils.toBoolean(value)) { config.dockerOptions = new DockerOptions(); diff --git a/org.lflang/src/org/lflang/generator/GeneratorBase.xtend b/org.lflang/src/org/lflang/generator/GeneratorBase.xtend index 12b53ec62c..2801003246 100644 --- a/org.lflang/src/org/lflang/generator/GeneratorBase.xtend +++ b/org.lflang/src/org/lflang/generator/GeneratorBase.xtend @@ -952,6 +952,34 @@ abstract class GeneratorBase extends AbstractLFValidator implements TargetTypes return false } + /** + * Copy the core files needed to build the RTI within a container. + * + * @param the directory where rti.Dockerfile is located. + * @param the core files used for code generation in the current target. + */ + def copyRtiFiles(File rtiDir, ArrayList<String> coreFiles) { + var rtiFiles = newArrayList() + rtiFiles.addAll(coreFiles) + + // add the RTI files on top of the coreFiles + rtiFiles.addAll( + "federated/RTI/rti.h", + "federated/RTI/rti.c", + "federated/RTI/CMakeLists.txt" + ) + fileConfig.copyFilesFromClassPath("/lib/c/reactor-c/core", rtiDir + File.separator + "core", rtiFiles) + } + + /** + * Write a Dockerfile for the current federate as given by filename. + * @param the name given to the docker file (without any extension). + */ + def writeDockerFile(String dockerFileName) { + throw new UnsupportedOperationException("This target does not support docker file generation.") + } + + /** * Parsed error message from a compiler is returned here. */ diff --git a/org.lflang/src/org/lflang/generator/PythonGenerator.xtend b/org.lflang/src/org/lflang/generator/PythonGenerator.xtend index 8e2473dbc5..fdcc33644d 100644 --- a/org.lflang/src/org/lflang/generator/PythonGenerator.xtend +++ b/org.lflang/src/org/lflang/generator/PythonGenerator.xtend @@ -1059,7 +1059,7 @@ class PythonGenerator extends CGenerator { targetConfig.noCompile = compileStatus if (errorsOccurred) return; - + var baseFileName = topLevelName for (federate : federates) { if (isFederated) { @@ -1092,7 +1092,6 @@ class PythonGenerator extends CGenerator { printRunInfo(); } } - } // Restore filename topLevelName = baseFileName @@ -1699,6 +1698,50 @@ class PythonGenerator extends CGenerator { } } + /** + * Write a Dockerfile for the current federate as given by filename. + * The file will go into src-gen/filename.Dockerfile. + * If there is no main reactor, then no Dockerfile will be generated + * (it wouldn't be very useful). + * @param The root filename (without any extension). + */ + override writeDockerFile(String filename) { + var srcGenPath = fileConfig.getSrcGenPath + val dockerFile = srcGenPath + File.separator + filename + '.Dockerfile' + // If a dockerfile exists, remove it. + var file = new File(dockerFile) + if (file.exists) { + file.delete + } + + if (this.mainDef === null) { + return + } + + val contents = new StringBuilder() + pr(contents, ''' + # Generated docker file for «topLevelName».lf in «srcGenPath». + # For instructions, see: https://github.com/icyphy/lingua-franca/wiki/Containerized-Execution + FROM python:alpine + WORKDIR /lingua-franca/«topLevelName» + COPY . src-gen + RUN set -ex && apk add --no-cache gcc musl-dev \ + && cd src-gen && python3 setup.py install && cd .. \ + && apk del gcc musl-dev + ENTRYPOINT ["python3", "src-gen/«filename».py"] + ''') + writeSourceCodeToFile(contents.toString.getBytes, dockerFile) + println("Dockerfile written to " + dockerFile) + println(''' + ##################################### + To build the docker image, use: + + docker build -t «topLevelName.toLowerCase()» -f «dockerFile» «srcGenPath» + + ##################################### + ''') + } + /** * Convert C types to formats used in Py_BuildValue and PyArg_PurseTuple. * This is unused but will be useful to enable inter-compatibility between diff --git a/org.lflang/src/org/lflang/generator/c/CGenerator.xtend b/org.lflang/src/org/lflang/generator/c/CGenerator.xtend index cfee8c2b07..be5a948fc8 100644 --- a/org.lflang/src/org/lflang/generator/c/CGenerator.xtend +++ b/org.lflang/src/org/lflang/generator/c/CGenerator.xtend @@ -27,6 +27,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package org.lflang.generator.c import java.io.File +import java.nio.file.Path import java.util.ArrayList import java.util.Collection import java.util.LinkedHashMap @@ -537,6 +538,14 @@ class CGenerator extends GeneratorBase { "federated/clock-sync.c" ); createFederatedLauncher(coreFiles); + + var rtiPath = fileConfig.getSrcGenBasePath().resolve("RTI") + var rtiDir = rtiPath.toFile() + if (!rtiDir.exists()) { + rtiDir.mkdirs() + } + writeRTIDockerFile(rtiPath, rtiDir) + copyRtiFiles(rtiDir, coreFiles) } // Perform distinct code generation into distinct files for each federate. @@ -1162,13 +1171,6 @@ class CGenerator extends GeneratorBase { // for more detail. if ((OS.indexOf("mac") >= 0) || (OS.indexOf("darwin") >= 0)) { // Mac support - coreFiles.add("platform/lf_POSIX_threads_support.c") - coreFiles.add("platform/lf_C11_threads_support.c") - coreFiles.add("platform/lf_POSIX_threads_support.h") - coreFiles.add("platform/lf_C11_threads_support.h") - coreFiles.add("platform/lf_macos_support.c") - coreFiles.add("platform/lf_macos_support.h") - coreFiles.add("platform/lf_unix_clock_support.c") // If there is no main reactor, then compilation will produce a .o file requiring further linking. // Also, if useCmake is set to true, we don't need to add platform files. The CMakeLists.txt file // will detect and use the appropriate platform file based on the platform that cmake is invoked on. @@ -1179,12 +1181,6 @@ class CGenerator extends GeneratorBase { } } else if (OS.indexOf("win") >= 0) { // Windows support - coreFiles.add("platform/lf_C11_threads_support.c") - coreFiles.add("platform/lf_C11_threads_support.h") - coreFiles.add("platform/lf_windows_support.c") - coreFiles.add("platform/lf_windows_support.h") - // For 64-bit epoch time - coreFiles.add("platform/lf_unix_clock_support.c") // If there is no main reactor, then compilation will produce a .o file requiring further linking. // Also, if useCmake is set to true, we don't need to add platform files. The CMakeLists.txt file // will detect and use the appropriate platform file based on the platform that cmake is invoked on. @@ -1195,13 +1191,6 @@ class CGenerator extends GeneratorBase { } } else if (OS.indexOf("nux") >= 0) { // Linux support - coreFiles.add("platform/lf_POSIX_threads_support.c") - coreFiles.add("platform/lf_C11_threads_support.c") - coreFiles.add("platform/lf_POSIX_threads_support.h") - coreFiles.add("platform/lf_C11_threads_support.h") - coreFiles.add("platform/lf_linux_support.c") - coreFiles.add("platform/lf_linux_support.h") - coreFiles.add("platform/lf_unix_clock_support.c") // If there is no main reactor, then compilation will produce a .o file requiring further linking. // Also, if useCmake is set to true, we don't need to add platform files. The CMakeLists.txt file // will detect and use the appropriate platform file based on the platform that cmake is invoked on. @@ -1213,6 +1202,21 @@ class CGenerator extends GeneratorBase { } else { errorReporter.reportError("Platform " + OS + " is not supported") } + + coreFiles.addAll( + "platform/lf_POSIX_threads_support.c", + "platform/lf_C11_threads_support.c", + "platform/lf_C11_threads_support.h", + "platform/lf_POSIX_threads_support.h", + "platform/lf_POSIX_threads_support.c", + "platform/lf_unix_clock_support.c", + "platform/lf_macos_support.c", + "platform/lf_macos_support.h", + "platform/lf_windows_support.c", + "platform/lf_windows_support.h", + "platform/lf_linux_support.c", + "platform/lf_linux_support.h" + ) } /** @@ -1239,24 +1243,27 @@ class CGenerator extends GeneratorBase { * The file will go into src-gen/filename.Dockerfile. * If there is no main reactor, then no Dockerfile will be generated * (it wouldn't be very useful). - * @param The root filename (without any extension). + * @param the name given to the docker file (without any extension). */ - def writeDockerFile(String filename) { - if (this.mainDef === null) { - return - } - + override writeDockerFile(String dockerFileName) { var srcGenPath = fileConfig.getSrcGenPath - val dockerFile = srcGenPath + File.separator + filename + '.Dockerfile' - val contents = new StringBuilder() - + val dockerFile = srcGenPath + File.separator + dockerFileName + '.Dockerfile' // If a dockerfile exists, remove it. var file = new File(dockerFile) if (file.exists) { file.delete } - // The Docker configuration uses gcc, so config.compiler is ignored here. - var compileCommand = '''gcc «targetConfig.compilerFlags.join(" ")» src-gen/«filename».c -o bin/«filename»''' + if (this.mainDef === null) { + return + } + + val contents = new StringBuilder() + // The Docker configuration uses cmake, so config.compiler is ignored here. + var compileCommand = ''' + cmake -S src-gen -B bin && \ + cd bin && \ + make all + ''' if (!targetConfig.buildCommands.nullOrEmpty) { compileCommand = targetConfig.buildCommands.join(' ') } @@ -1264,27 +1271,96 @@ class CGenerator extends GeneratorBase { if (!targetConfig.fileNames.nullOrEmpty) { additionalFiles = '''COPY "«targetConfig.fileNames.join('" "')»" "src-gen/"''' } + var dockerCompiler = 'gcc' + var fileExtension = 'c' + + if (CCppMode) { + dockerCompiler = 'g++' + fileExtension = 'cpp' + } + pr(contents, ''' - # Generated docker file for «topLevelName».lf in «srcGenPath». + # Generated docker file for «topLevelName» in «srcGenPath». # For instructions, see: https://github.com/icyphy/lingua-franca/wiki/Containerized-Execution - FROM «targetConfig.dockerOptions.from» - WORKDIR /lingua-franca - COPY src-gen/core src-gen/core - COPY "src-gen/«filename».c" "src-gen/ctarget.h" "src-gen/" + FROM «targetConfig.dockerOptions.from» AS builder + WORKDIR /lingua-franca/«topLevelName» + RUN set -ex && apk add --no-cache «dockerCompiler» musl-dev cmake make + COPY core src-gen/core + COPY ctarget.h ctarget.c src-gen/ + COPY CMakeLists.txt \ + «topLevelName».«fileExtension» src-gen/ «additionalFiles» RUN set -ex && \ - apk add --no-cache gcc musl-dev && \ mkdir bin && \ - «compileCommand» && \ - apk del gcc musl-dev && \ - rm -rf src-gen + «compileCommand» + + FROM «targetConfig.dockerOptions.from» + WORKDIR /lingua-franca + RUN mkdir bin + COPY --from=builder /lingua-franca/«topLevelName»/bin/«topLevelName» ./bin/«topLevelName» + # Use ENTRYPOINT not CMD so that command-line arguments go through - ENTRYPOINT ["./bin/«filename»"] + ENTRYPOINT ["./bin/«topLevelName»"] ''') writeSourceCodeToFile(contents.toString.getBytes, dockerFile) - println("Dockerfile written to " + dockerFile) + println('''Dockerfile for «topLevelName» written to ''' + dockerFile) + println(''' + ##################################### + To build the docker image, use: + + docker build -t «topLevelName.toLowerCase()» -f «dockerFile» «srcGenPath» + + ##################################### + ''') } - + + /** + * Write a Dockerfile for the RTI at rtiDir. + * The file will go into src-gen/RTI/rti.Dockerfile. + * @param the directory where rti.Dockerfile will be written to. + */ + def writeRTIDockerFile(Path rtiPath, File rtiDir) { + val dockerFileName = 'rti.Dockerfile' + val dockerFile = rtiDir + File.separator + dockerFileName + var srcGenPath = fileConfig.getSrcGenPath + // If a dockerfile exists, remove it. + var file = new File(dockerFile) + if (file.exists) { + file.delete + } + if (this.mainDef === null) { + return + } + val contents = new StringBuilder() + pr(contents, ''' + # Generated docker file for RTI in «rtiDir». + # For instructions, see: https://github.com/icyphy/lingua-franca/wiki/Containerized-Execution + FROM alpine:latest + WORKDIR /lingua-franca/RTI + COPY core core + WORKDIR core/federated/RTI + RUN set -ex && apk add --no-cache gcc musl-dev cmake make && \ + mkdir build && \ + cd build && \ + cmake ../ && \ + make && \ + make install + + # Use ENTRYPOINT not CMD so that command-line arguments go through + ENTRYPOINT ["./build/RTI"] + ''') + writeSourceCodeToFile(contents.toString.getBytes, dockerFile) + println("Dockerfile for RTI written to " + dockerFile) + println(''' + ##################################### + To build the docker image, use: + + docker build -t rti -f «dockerFile» «rtiDir» + + ##################################### + ''') + } + /** * Initialize clock synchronization (if enabled) and its related options for a given federate. * @@ -1438,7 +1514,7 @@ class CGenerator extends GeneratorBase { lf_thread_create(&_fed.inbound_p2p_handling_thread_id, handle_p2p_connections_from_federates, NULL); ''') } - + for (remoteFederate : federate.outboundP2PConnections) { pr('''connect_to_federate(«remoteFederate.id»);''') }