diff --git a/org.lflang/src/org/lflang/federated/launcher/FedLauncher.java b/org.lflang/src/org/lflang/federated/launcher/FedLauncher.java index c4643d4bd7..f89648256e 100644 --- a/org.lflang/src/org/lflang/federated/launcher/FedLauncher.java +++ b/org.lflang/src/org/lflang/federated/launcher/FedLauncher.java @@ -211,7 +211,6 @@ public void createLauncher( if(distCode.length() == 0) distCode.append(distHeader + "\n"); String logFileName = String.format("log/%s_%s.log", fedFileConfig.name, federate.name); String compileCommand = compileCommandForFederate(federate); - // '''«targetConfig.compiler» src-gen/«topLevelName»_«federate.name».c -o bin/«topLevelName»_«federate.name» -pthread «targetConfig.compilerFlags.join(" ")»''' // FIXME: Should $FEDERATION_ID be used to ensure unique directories, executables, on the remote host? distCode.append(getDistCode(path, federate, fedRelSrcGenPath, logFileName, fedFileConfig, compileCommand) + "\n"); String executeCommand = executeCommandForRemoteFederate(federate); diff --git a/org.lflang/src/org/lflang/generator/DockerComposeGenerator.java b/org.lflang/src/org/lflang/generator/DockerComposeGenerator.java deleted file mode 100644 index ff756298e6..0000000000 --- a/org.lflang/src/org/lflang/generator/DockerComposeGenerator.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.lflang.generator; - -import java.io.File; -import java.io.IOException; -import org.eclipse.xtext.xbase.lib.Exceptions; - -/** - * Generates the content of the docker-compose.yml file - * used to deploy containerized LF programs. - * - * @author{Hou Seng Wong } - */ -public class DockerComposeGenerator { - /** - * Write the docker-compose.yml for orchestrating the federates. - * @param the directory to write the docker-compose.yml - * @param content of the "services" section of the docker-compose.yml - * @param the name of the network hosting the federation - */ - public static void writeFederatesDockerComposeFile( - File dir, - StringBuilder dockerComposeServices, - String networkName - ) { - var dockerComposeFileName = "docker-compose.yml"; - var dockerComposeFile = dir + File.separator + dockerComposeFileName; - var contents = new CodeBuilder(); - contents.pr(String.join("\n", - "version: \"3.9\"", - "services:", - dockerComposeServices.toString(), - "networks:", - " lingua-franca:", - " name: "+networkName - )); - try { - contents.writeToFile(dockerComposeFile); - } catch (IOException e) { - throw Exceptions.sneakyThrow(e); - } - } - - /** - * Append a service to the "services" section of the docker-compose.yml file. - * @param the content of the "services" section of the docker-compose.yml file. - * @param the name of the federate to be added to "services". - * @param the name of the federate's Dockerfile. - */ - public static void appendFederateToDockerComposeServices(StringBuilder dockerComposeServices, String federateName, String context, String dockerFileName) { - var tab = " ".repeat(4); - dockerComposeServices.append(tab+federateName+":\n"); - dockerComposeServices.append(tab+tab+"build:\n"); - dockerComposeServices.append(tab+tab+tab+"context: "+context+"\n"); - dockerComposeServices.append(tab+tab+tab+"dockerfile: "+dockerFileName+"\n"); - dockerComposeServices.append(tab+tab+"command: -i 1\n"); - } - - /** - * Append the RTI to the "services" section of the docker-compose.yml file. - * @param the content of the "services" section of the docker-compose.yml file. - * @param the name given to the RTI in the "services" section. - * @param the tag of the RTI's image. - * @param the number of federates. - */ - public static void appendRtiToDockerComposeServices(StringBuilder dockerComposeServices, String dockerImageName, String hostName, int n) { - var tab = " ".repeat(4); - dockerComposeServices.append(tab+"rti:\n"); - dockerComposeServices.append(tab+tab+"image: "+dockerImageName+"\n"); - dockerComposeServices.append(tab+tab+"hostname: "+hostName+"\n"); - dockerComposeServices.append(tab+tab+"command: -i 1 -n "+n+"\n"); - } -} diff --git a/org.lflang/src/org/lflang/generator/DockerGeneratorBase.java b/org.lflang/src/org/lflang/generator/DockerGeneratorBase.java new file mode 100644 index 0000000000..bf5e92f070 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/DockerGeneratorBase.java @@ -0,0 +1,249 @@ +package org.lflang.generator; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import org.lflang.FileConfig; +import org.lflang.TargetConfig; +import org.lflang.util.FileUtil; + +/** + * The base class for docker file related code generation. + * + * @author{Hou Seng Wong } + */ +public class DockerGeneratorBase { + /** + * The docker compose services representing each federate. + * Ideally, this would be a list of Strings instead of a StringBuilder. + */ + protected StringBuilder composeServices; + + /** + * A docker file will be generated for each lingua franca module. + * This maps the name of the LF module to the data related to the docker + * file for that module. + */ + protected Map> moduleNameToData; + + /** + * Indicates whether or not the program is federated. + */ + protected final boolean isFederated; + + /** + * The number of federates this docker generator have added using `addFederate` so far. + */ + protected int nFederates; + + /** + * In federated execution, the host of the rti. + */ + protected String host = null; + + /** + * Generates the docker file related code for the Python target. + * The type specified in the following javadoc refers to the + * type of the object stored in `moduleNameToData.get(lfModuleName)` + */ + protected enum Key { + /** + * A `Path` object that is the absolute path to the docker file. + */ + DOCKER_FILE_PATH, + /** + * A `String` object that is the content of the docker file + * to be generated. + */ + DOCKER_FILE_CONTENT, + /** + * A `String` object that is the name of the docker compose + * service for the LF module. + */ + DOCKER_COMPOSE_SERVICE_NAME, + /** + * A `String` object that is the build context of the + * docker container. + */ + DOCKER_BUILD_CONTEXT, + } + + /** + * The constructor for the base docker file generation class. + * @param isFederated True if federated execution. False otherwise. + */ + public DockerGeneratorBase(boolean isFederated) { + moduleNameToData = new HashMap<>(); + composeServices = new StringBuilder(); + this.isFederated = isFederated; + nFederates = 0; + } + + /** + * Set the `host` of the container + * that launches the RTI. + * @param host The host to set. + */ + public void setHost(String host) { + this.host = host; + } + + /** + * Add a federate to the list of federates to generate docker files for. + * + * @param lfModuleName The module name of the federate. + * @param federateName The name of the federate's reactor. + * @param dockerFilePath The path where the docker file will be written. + * @param targetConfig The target config. + */ + public void addFederate( + String lfModuleName, + String federateName, + Path dockerFilePath, + TargetConfig targetConfig + ) { + Map k = new HashMap<>(); + k.put(Key.DOCKER_FILE_PATH, dockerFilePath); + var dockerFileContent = generateDockerFileContent(lfModuleName); + k.put(Key.DOCKER_FILE_CONTENT, dockerFileContent); + k.put(Key.DOCKER_COMPOSE_SERVICE_NAME, isFederated ? federateName : lfModuleName.toLowerCase()); + k.put(Key.DOCKER_BUILD_CONTEXT, isFederated ? federateName : "."); + moduleNameToData.put(lfModuleName, k); + nFederates++; + appendFederateToDockerComposeServices( + composeServices, + (String) k.get(Key.DOCKER_COMPOSE_SERVICE_NAME), + (String) k.get(Key.DOCKER_BUILD_CONTEXT), + dockerFilePath.getFileName().toString()); + } + + /** + * Write the docker files generated for the federates added using `addFederate` so far. + * + * @param dockerComposeFilePath The path where the docker compose file will be written. + */ + public void writeDockerFiles(Path dockerComposeFilePath) throws IOException { + for (String lfModuleName : moduleNameToData.keySet()) { + var k = moduleNameToData.get(lfModuleName); + var dockerFilePath = (Path) k.get(Key.DOCKER_FILE_PATH); + if (dockerFilePath.toFile().exists()) { + dockerFilePath.toFile().delete(); + } + var contents = (String) k.get(Key.DOCKER_FILE_CONTENT); + FileUtil.writeToFile(contents, dockerFilePath); + System.out.println(getDockerBuildCommand( + lfModuleName, dockerFilePath, + dockerComposeFilePath.getParent(), + (String) k.get(Key.DOCKER_COMPOSE_SERVICE_NAME))); + } + + if (isFederated && host != null) { + appendRtiToDockerComposeServices( + composeServices, + "lflang/rti:rti", + host, + nFederates + ); + } + writeFederatesDockerComposeFile(dockerComposeFilePath, composeServices, "lf"); + } + + /** + * A template function for generating target-specific docker file content. + * + * @param lfModuleName The name of the LF module currently generating. + */ + protected String generateDockerFileContent( + String lfModuleName + ) { + throw new UnsupportedOperationException("Docker file content is not implemented"); + } + + /** + * Write a Dockerfile for the current federate as given by filename. + * @param The directory where the docker compose file is generated. + * @param The name of the docker file. + * @param The name of the federate. + */ + public String getDockerComposeCommand() { + String OS = System.getProperty("os.name").toLowerCase(); + return (OS.indexOf("nux") >= 0) ? "docker-compose" : "docker compose"; + } + + /** + * Write a Dockerfile for the current federate as given by filename. + * @param The directory where the docker compose file is generated. + * @param The name of the docker file. + * @param The name of the federate. + */ + public String getDockerBuildCommand( + String lfModuleName, + Path dockerFilePath, + Path dockerComposeDir, + String dockerComposeServiceName + ) { + return String.join("\n", + "Dockerfile for "+lfModuleName+" written to "+dockerFilePath, + "#####################################", + "To build the docker image, go to "+dockerComposeDir+" and run:", + "", + " "+getDockerComposeCommand()+" build "+dockerComposeServiceName, + "", + "#####################################" + ); + } + + /** + * Write the docker-compose.yml for orchestrating the federates. + * @param the directory to write the docker-compose.yml + * @param content of the "services" section of the docker-compose.yml + * @param the name of the network hosting the federation + */ + private void writeFederatesDockerComposeFile( + Path dockerComposeFilePath, + StringBuilder dockerComposeServices, + String networkName + ) throws IOException { + var contents = new CodeBuilder(); + contents.pr(String.join("\n", + "version: \"3.9\"", + "services:", + dockerComposeServices.toString(), + "networks:", + " lingua-franca:", + " name: "+networkName + )); + FileUtil.writeToFile(contents.toString(), dockerComposeFilePath); + } + + /** + * Append a service to the "services" section of the docker-compose.yml file. + * @param the content of the "services" section of the docker-compose.yml file. + * @param the name of the federate to be added to "services". + * @param the name of the federate's Dockerfile. + */ + private void appendFederateToDockerComposeServices(StringBuilder dockerComposeServices, String federateName, String context, String dockerFileName) { + var tab = " ".repeat(4); + dockerComposeServices.append(tab+federateName+":\n"); + dockerComposeServices.append(tab+tab+"build:\n"); + dockerComposeServices.append(tab+tab+tab+"context: "+context+"\n"); + dockerComposeServices.append(tab+tab+tab+"dockerfile: "+dockerFileName+"\n"); + dockerComposeServices.append(tab+tab+"command: -i 1\n"); + } + + /** + * Append the RTI to the "services" section of the docker-compose.yml file. + * @param the content of the "services" section of the docker-compose.yml file. + * @param the name given to the RTI in the "services" section. + * @param the tag of the RTI's image. + * @param the number of federates. + */ + private void appendRtiToDockerComposeServices(StringBuilder dockerComposeServices, String dockerImageName, String hostName, int n) { + var tab = " ".repeat(4); + dockerComposeServices.append(tab+"rti:\n"); + dockerComposeServices.append(tab+tab+"image: "+dockerImageName+"\n"); + dockerComposeServices.append(tab+tab+"hostname: "+hostName+"\n"); + dockerComposeServices.append(tab+tab+"command: -i 1 -n "+n+"\n"); + } +} diff --git a/org.lflang/src/org/lflang/generator/GeneratorBase.java b/org.lflang/src/org/lflang/generator/GeneratorBase.java index 9cb1c47c50..7e71b32e0f 100644 --- a/org.lflang/src/org/lflang/generator/GeneratorBase.java +++ b/org.lflang/src/org/lflang/generator/GeneratorBase.java @@ -24,7 +24,6 @@ ***************/ package org.lflang.generator; -import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -38,7 +37,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; - import org.eclipse.core.resources.IMarker; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.resource.Resource; @@ -234,11 +232,6 @@ public abstract class GeneratorBase extends AbstractLFValidator { */ protected String classpathLF; - /** - * The name of the top-level reactor. - */ - protected String topLevelName; // FIXME: remove and use fileConfig.name instead - // ////////////////////////////////////////// // // Private fields. @@ -247,7 +240,6 @@ public abstract class GeneratorBase extends AbstractLFValidator { */ public GeneratorBase(FileConfig fileConfig, ErrorReporter errorReporter) { this.fileConfig = fileConfig; - this.topLevelName = fileConfig.name; this.errorReporter = errorReporter; this.commandFactory = new GeneratorCommandFactory(errorReporter, fileConfig); } @@ -787,45 +779,6 @@ public boolean isFederatedAndCentralized() { return isFederated && targetConfig.coordination == CoordinationType.CENTRALIZED; } - /** - * Write a Dockerfile for the current federate as given by filename. - * @param The directory where the docker compose file is generated. - * @param The name of the docker file. - * @param The name of the federate. - */ - public void writeDockerFile(File dockerComposeDir, String dockerFileName, String federateName) throws IOException { - throw new UnsupportedOperationException("This target does not support docker file generation."); - } - - /** - * Write a Dockerfile for the current federate as given by filename. - * @param The directory where the docker compose file is generated. - * @param The name of the docker file. - * @param The name of the federate. - */ - public String getDockerComposeCommand() { - String OS = System.getProperty("os.name").toLowerCase(); - return (OS.indexOf("nux") >= 0) ? "docker-compose" : "docker compose"; - } - - /** - * Write a Dockerfile for the current federate as given by filename. - * @param The directory where the docker compose file is generated. - * @param The name of the docker file. - * @param The name of the federate. - */ - public String getDockerBuildCommand(String dockerFile, File dockerComposeDir, String federateName) { - return String.join("\n", - "Dockerfile for "+topLevelName+" written to "+dockerFile, - "#####################################", - "To build the docker image, go to "+dockerComposeDir+" and run:", - "", - " "+getDockerComposeCommand()+" build "+federateName, - "", - "#####################################" - ); - } - /** * Parsed error message from a compiler is returned here. */ diff --git a/org.lflang/src/org/lflang/generator/c/CDockerGenerator.java b/org.lflang/src/org/lflang/generator/c/CDockerGenerator.java index 328732fe0b..6a6cb6b7d3 100644 --- a/org.lflang/src/org/lflang/generator/c/CDockerGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CDockerGenerator.java @@ -1,26 +1,47 @@ package org.lflang.generator.c; -import java.nio.file.Path; +import org.eclipse.xtext.xbase.lib.IterableExtensions; +import org.lflang.TargetConfig; +import org.lflang.generator.DockerGeneratorBase; +import org.lflang.util.StringUtil; /** - * Generates the docker file related code for the C and CCpp target. - * + * Generate the docker file related code for the C and CCpp target. + * * @author{Edward A. Lee } * @author{Hou Seng Wong } */ -public class CDockerGenerator { - public static String generateDockerFileContent( - String topLevelName, - String baseImage, - String compiler, - String compileCommand, - Path srcGenPath +public class CDockerGenerator extends DockerGeneratorBase { + private boolean CCppMode; + private TargetConfig targetConfig; + private final String defaultBaseImage = "alpine:latest"; + + public CDockerGenerator(boolean isFederated, boolean CCppMode, TargetConfig targetConfig) { + super(isFederated); + this.CCppMode = CCppMode; + this.targetConfig = targetConfig; + } + + /** + * Generate the contents of the docker file. + * + * @param lfModuleName The name of the lingua franca module. + * In unfederated execution, this is fileConfig.name. + * In federated execution, this is typically fileConfig.name + "_" + federate.name + */ + @Override + protected String generateDockerFileContent( + String lfModuleName ) { - return String.join("\n", - "# Generated docker file for "+topLevelName+" in "+srcGenPath+".", + var compileCommand = IterableExtensions.isNullOrEmpty(targetConfig.buildCommands) ? + generateDefaultCompileCommand() : + StringUtil.joinObjects(targetConfig.buildCommands, " "); + var compiler = CCppMode ? "g++" : "gcc"; + var baseImage = targetConfig.dockerOptions.from == null ? defaultBaseImage : targetConfig.dockerOptions.from; + return String.join("\n", "# For instructions, see: https://github.com/icyphy/lingua-franca/wiki/Containerized-Execution", "FROM "+baseImage+" AS builder", - "WORKDIR /lingua-franca/"+topLevelName, + "WORKDIR /lingua-franca/"+lfModuleName, "RUN set -ex && apk add --no-cache "+compiler+" musl-dev cmake make", "COPY . src-gen", compileCommand, @@ -28,15 +49,18 @@ public static String generateDockerFileContent( "FROM "+baseImage, "WORKDIR /lingua-franca", "RUN mkdir bin", - "COPY --from=builder /lingua-franca/"+topLevelName+"/bin/"+topLevelName+" ./bin/"+topLevelName, + "COPY --from=builder /lingua-franca/"+lfModuleName+"/bin/"+lfModuleName+" ./bin/"+lfModuleName, "", "# Use ENTRYPOINT not CMD so that command-line arguments go through", - "ENTRYPOINT [\"./bin/"+topLevelName+"\"]" + "ENTRYPOINT [\"./bin/"+lfModuleName+"\"]" ); } - public static String generateDefaultCompileCommand() { - return String.join("\n", + /** + * Return the default compile command for the C docker container. + */ + private String generateDefaultCompileCommand() { + return String.join("\n", "RUN set -ex && \\", "mkdir bin && \\", "cmake -S src-gen -B bin && \\", diff --git a/org.lflang/src/org/lflang/generator/c/CGenerator.java b/org.lflang/src/org/lflang/generator/c/CGenerator.java index 6bb25d69e6..fcf52fdee4 100644 --- a/org.lflang/src/org/lflang/generator/c/CGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CGenerator.java @@ -25,21 +25,17 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY ***************/ package org.lflang.generator.c; - import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; - import com.google.common.base.Objects; import com.google.common.collect.Iterables; - import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.xtext.util.CancelIndicator; import org.eclipse.xtext.xbase.lib.Exceptions; @@ -64,7 +60,7 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.generator.ActionInstance; import org.lflang.generator.CodeBuilder; import org.lflang.generator.GeneratorBase; -import org.lflang.generator.DockerComposeGenerator; +import org.lflang.generator.DockerGeneratorBase; import org.lflang.generator.GeneratorResult; import org.lflang.generator.IntegratedBuilder; import org.lflang.generator.GeneratorUtils; @@ -78,18 +74,6 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.generator.TargetTypes; import org.lflang.generator.TimerInstance; import org.lflang.generator.TriggerInstance; -import org.lflang.generator.c.CActionGenerator; -import org.lflang.generator.c.CTimerGenerator; -import org.lflang.generator.c.CStateGenerator; -import org.lflang.generator.c.CTracingGenerator; -import org.lflang.generator.c.CPortGenerator; -import org.lflang.generator.c.CModesGenerator; -import org.lflang.generator.c.CMainGenerator; -import org.lflang.generator.c.CFederateGenerator; -import org.lflang.generator.c.CNetworkGenerator; -import org.lflang.generator.c.CTriggerObjectsGenerator; -import org.lflang.generator.c.CConstructorGenerator; -import org.lflang.generator.c.InteractingContainedReactors; import org.lflang.lf.Action; import org.lflang.lf.ActionOrigin; import org.lflang.lf.Delay; @@ -367,9 +351,6 @@ public class CGenerator extends GeneratorBase { * Extra lines that need to go into the generated CMakeLists.txt. */ private String cMakeExtras = ""; - - /** The command to run the generated code if specified in the target directive. */ - private ArrayList runCommand = new ArrayList(); /** Place to collect code to execute at the start of a time step. */ private CodeBuilder startTimeStep = new CodeBuilder(); @@ -408,7 +389,7 @@ public CGenerator(FileConfig fileConfig, ErrorReporter errorReporter) { /** * Set C-specific default target configurations if needed. */ - public void setCSpecificDefaults(LFGeneratorContext context) { + public void setCSpecificDefaults() { if (!targetConfig.useCmake && StringExtensions.isNullOrEmpty(targetConfig.compiler)) { if (this.CCppMode) { targetConfig.compiler = "g++"; @@ -508,7 +489,9 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { if (!isOSCompatible()) return; // Incompatible OS and configuration // Perform set up that does not generate code - setUpParameters(context); + setUpGeneralParameters(); + + var commonCode = new CodeBuilder(code); // Create the output directories if they don't yet exist. var dir = fileConfig.getSrcGenPath().toFile(); @@ -517,14 +500,7 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { if (!dir.exists()) dir.mkdirs(); // Docker related paths - var dockerComposeDir = fileConfig.getSrcGenPath().toFile(); - var dockerComposeServices = new StringBuilder(); - - // Perform distinct code generation into distinct files for each federate. - var baseFilename = topLevelName; - - // Copy the code generated so far. - var commonCode = new CodeBuilder(code); + DockerGeneratorBase dockerGenerator = getDockerGenerator(); // Keep a separate file config for each federate var oldFileConfig = fileConfig; @@ -536,71 +512,33 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { ); var compileThreadPool = Executors.newFixedThreadPool(numOfCompileThreads); System.out.println("******** Using "+numOfCompileThreads+" threads to compile the program."); - var federateCount = 0; LFGeneratorContext generatingContext = new SubContext( context, IntegratedBuilder.VALIDATED_PERCENT_PROGRESS, IntegratedBuilder.GENERATED_PERCENT_PROGRESS ); + var federateCount = 0; for (FederateInstance federate : federates) { - currentFederate = federate; - federateCount++; - startTimeStepIsPresentCount = 0; - startTimeStepTokens = 0; - - // If federated, append the federate name to the file name. - // Only generate one output if there is no federation. + var lfModuleName = isFederated ? fileConfig.name + "_" + federate.name : fileConfig.name; + setUpFederateSpecificParameters(federate, commonCode); if (isFederated) { - topLevelName = baseFilename + "_" + federate.name; // FIXME: don't (temporarily) reassign a class variable for this + // If federated, append the federate name to the file name. + // Only generate one output if there is no federation. try { fileConfig = new FedFileConfig(fileConfig, federate.name); } catch (IOException e) { Exceptions.sneakyThrow(e); } - - // Reset the cmake-includes and files, to be repopulated for each federate individually. - // This is done to enable support for separately - // adding cmake-includes/files for different federates to prevent linking and mixing - // all federates' supporting libraries/files together. - targetConfig.cmakeIncludes.clear(); - targetConfig.cmakeIncludesWithoutPath.clear(); - targetConfig.fileNames.clear(); - targetConfig.filesNamesWithoutPath.clear(); - - // Re-apply the cmake-include target property of the main .lf file. - var target = GeneratorUtils.findTarget(mainDef.getReactorClass().eResource()); - if (target.getConfig() != null) { - // Update the cmake-include - TargetProperty.updateOne( - this.targetConfig, - TargetProperty.CMAKE_INCLUDE, - convertToEmptyListIfNull(target.getConfig().getPairs()), - errorReporter - ); - // Update the files - TargetProperty.updateOne( - this.targetConfig, - TargetProperty.FILES, - convertToEmptyListIfNull(target.getConfig().getPairs()), - errorReporter - ); - } - - // Need to copy user files again since the source structure changes - // for federated programs. - copyUserFiles(this.targetConfig, this.fileConfig); - - // Clear out previously generated code. - code = new CodeBuilder(commonCode); - initializeTriggerObjects = new CodeBuilder(); - - // Enable clock synchronization if the federate - // is not local and clock-sync is enabled - initializeClockSynchronization(); - - - startTimeStep = new CodeBuilder(); } + generateCodeForCurrentFederate(lfModuleName); + // Derive target filename from the .lf filename. + var cFilename = CCompiler.getTargetFileName(lfModuleName, this.CCppMode); + var targetFile = fileConfig.getSrcGenPath() + File.separator + cFilename; try { + if (isFederated) { + // Need to copy user files again since the source structure changes + // for federated programs. + copyUserFiles(this.targetConfig, this.fileConfig); + } // Copy the core lib FileUtil.copyFilesFromClassPath( "/lib/c/reactor-c/core", @@ -613,190 +551,27 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { ); // Copy the C target files copyTargetFiles(); + // Write the generated code + code.writeToFile(targetFile); } catch (IOException e) { Exceptions.sneakyThrow(e); } - generateDirectives(); - generateTopLevelPreambles(); - code.pr(CMainGenerator.generateCode()); - // Generate code for each reactor. - generateReactorDefinitions(); - - // Derive target filename from the .lf filename. - var cFilename = CCompiler.getTargetFileName(topLevelName, this.CCppMode); - - var file = fileConfig.getSrcGenPath().resolve(cFilename).toFile(); - // Delete source previously produced by the LF compiler. - if (file.exists()) { - file.delete(); - } - - // Delete binary previously produced by the C compiler. - file = fileConfig.binPath.resolve(topLevelName).toFile(); - if (file.exists()) { - file.delete(); - } - - // Generate main instance, if there is one. - // Note that any main reactors in imported files are ignored. - // Skip generation if there are cycles. - if (this.main != null) { - initializeTriggerObjects.pr(String.join("\n", - "int _lf_startup_reactions_count = 0;", - "int _lf_shutdown_reactions_count = 0;", - "int _lf_timer_triggers_count = 0;", - "int _lf_tokens_with_ref_count_count = 0;" - )); - - // Create an array of arrays to store all self structs. - // This is needed because connections cannot be established until - // all reactor instances have self structs because ports that - // receive data reference the self structs of the originating - // reactors, which are arbitarily far away in the program graph. - generateSelfStructs(main); - - generateReactorInstance(this.main); - // Generate function to set default command-line options. - // A literal array needs to be given outside any function definition, - // so start with that. - if (runCommand.size() > 0) { - code.pr(String.join("\n", - "char* _lf_default_argv[] = { " + addDoubleQuotes(joinObjects(runCommand, addDoubleQuotes(", ")))+" };", - "void _lf_set_default_command_line_options() {", - " default_argc = "+runCommand.size()+";", - " default_argv = _lf_default_argv;", - "}" - )); - } else { - code.pr("void _lf_set_default_command_line_options() {}"); - } - - // If there are timers, create a table of timers to be initialized. - code.pr(CTimerGenerator.generateDeclarations(timerCount)); - - // If there are shutdown reactions, create a table of triggers. - code.pr(CReactionGenerator.generateShutdownTriggersTable(shutdownReactionCount)); - - // If there are modes, create a table of mode state to be checked for transitions. - code.pr(CModesGenerator.generateModeStatesTable( - hasModalReactors, - modalReactorCount, - modalStateResetCount - )); - - // Generate function to return a pointer to the action trigger_t - // that handles incoming network messages destined to the specified - // port. This will only be used if there are federates. - if (federate.networkMessageActions.size() > 0) { - // Create a static array of trigger_t pointers. - // networkMessageActions is a list of Actions, but we - // need a list of trigger struct names for ActionInstances. - // There should be exactly one ActionInstance in the - // main reactor for each Action. - var triggers = new LinkedList(); - for (Action action : federate.networkMessageActions) { - // Find the corresponding ActionInstance. - var actionInstance = main.lookupActionInstance(action); - triggers.add(CUtil.triggerRef(actionInstance, null)); - } - var actionTableCount = 0; - for (String trigger : triggers) { - initializeTriggerObjects.pr("_lf_action_table["+(actionTableCount++)+"] = &"+trigger+";"); - } - code.pr(String.join("\n", - "trigger_t* _lf_action_table["+federate.networkMessageActions.size()+"];", - "trigger_t* _lf_action_for_port(int port_id) {", - " if (port_id < "+federate.networkMessageActions.size()+") {", - " return _lf_action_table[port_id];", - " } else {", - " return NULL;", - " }", - "}" - )); - } else { - code.pr(String.join("\n", - "trigger_t* _lf_action_for_port(int port_id) {", - " return NULL;", - "}" - )); - } - - // Generate function to initialize the trigger objects for all reactors. - code.pr(CTriggerObjectsGenerator.generateInitializeTriggerObjects( - federate, - main, - targetConfig, - initializeTriggerObjects, - startTimeStep, - types, - topLevelName, - federationRTIProperties, - startTimeStepTokens, - startTimeStepIsPresentCount, - startupReactionCount, - isFederated, - isFederatedAndDecentralized(), - clockSyncIsOn() - )); - - // Generate function to trigger startup reactions for all reactors. - code.pr(CReactionGenerator.generateLfTriggerStartupReactions(startupReactionCount)); - - // Generate function to schedule timers for all reactors. - code.pr(CTimerGenerator.generateLfInitializeTimer(timerCount)); - - // Generate a function that will either do nothing - // (if there is only one federate or the coordination - // is set to decentralized) or, if there are - // downstream federates, will notify the RTI - // that the specified logical time is complete. - code.pr(String.join("\n", - "void logical_tag_complete(tag_t tag_to_send) {", - (isFederatedAndCentralized() ? - " _lf_logical_tag_complete(tag_to_send);" : "" - ), - "}" - )); - - if (isFederated) { - code.pr(CFederateGenerator.generateFederateNeighborStructure(federate).toString()); - } - - // Generate function to schedule shutdown reactions if any - // reactors have reactions to shutdown. - code.pr(CReactionGenerator.generateLfTriggerShutdownReactions( - shutdownReactionCount - )); - - // Generate an empty termination function for non-federated - // execution. For federated execution, an implementation is - // provided in federate.c. That implementation will resign - // from the federation and close any open sockets. - if (!isFederated) { - code.pr("void terminate_execution() {}"); - } - - code.pr(CModesGenerator.generateLfHandleModeChanges( - hasModalReactors, - modalStateResetCount - )); - } - var targetFile = fileConfig.getSrcGenPath() + File.separator + cFilename; - try { - code.writeToFile(targetFile); - } catch (IOException e) { - Exceptions.sneakyThrow(e); + // Create docker file. + if (targetConfig.dockerOptions != null && mainDef != null) { + dockerGenerator.addFederate( + lfModuleName, federate.name, + fileConfig.getSrcGenPath().resolve(lfModuleName + ".Dockerfile"), + targetConfig); } - if (targetConfig.useCmake) { // If cmake is requested, generated the CMakeLists.txt var cmakeGenerator = new CCmakeGenerator(targetConfig, fileConfig); var cmakeFile = fileConfig.getSrcGenPath() + File.separator + "CMakeLists.txt"; var cmakeCode = cmakeGenerator.generateCMakeCode( List.of(cFilename), - topLevelName, + lfModuleName, errorReporter, CCppMode, mainDef != null, @@ -808,22 +583,6 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { Exceptions.sneakyThrow(e); } } - - // Create docker file. - if (targetConfig.dockerOptions != null) { - var dockerFileName = topLevelName + ".Dockerfile"; - try { - if (isFederated) { - writeDockerFile(dockerComposeDir, dockerFileName, federate.name); - DockerComposeGenerator.appendFederateToDockerComposeServices(dockerComposeServices, federate.name, federate.name, dockerFileName); - } else { - writeDockerFile(dockerComposeDir, dockerFileName, topLevelName.toLowerCase()); - DockerComposeGenerator.appendFederateToDockerComposeServices(dockerComposeServices, topLevelName.toLowerCase(), ".", dockerFileName); - } - } catch (IOException e) { - Exceptions.sneakyThrow(e); - } - } // 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. @@ -841,10 +600,11 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { // so that compilation can happen in parallel. var cleanCode = code.removeLines("#line"); - var execName = topLevelName; + 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 = CCppMode; + federateCount++; generatingContext.reportProgress( String.format("Generated code for %d/%d executables. Compiling...", federateCount, federates.size()), 100 * federateCount / federates.size() @@ -879,18 +639,6 @@ public void run() { } fileConfig = oldFileConfig; } - - if (targetConfig.dockerOptions != null) { - if (isFederated) { - DockerComposeGenerator.appendRtiToDockerComposeServices( - dockerComposeServices, - "lflang/rti:rti", - federationRTIProperties.get("host").toString(), - federates.size() - ); - } - DockerComposeGenerator.writeFederatesDockerComposeFile(dockerComposeDir, dockerComposeServices, "lf"); - } // Initiate an orderly shutdown in which previously submitted tasks are // executed, but no new tasks will be accepted. @@ -902,9 +650,6 @@ public void run() { } catch (Exception e) { Exceptions.sneakyThrow(e); } - - // Restore the base filename. - topLevelName = baseFilename; if (isFederated) { try { @@ -914,6 +659,18 @@ public void run() { } } + if (targetConfig.dockerOptions != null && mainDef != null) { + if (isFederated) { + dockerGenerator.setHost(federationRTIProperties.get("host").toString()); + } + try { + dockerGenerator.writeDockerFiles( + fileConfig.getSrcGenPath().resolve("docker-compose.yml")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + // If a build directive has been given, invoke it now. // Note that the code does not get cleaned in this case. if (!targetConfig.noCompile) { @@ -942,6 +699,152 @@ public void run() { // In case we are in Eclipse, make sure the generated code is visible. GeneratorUtils.refreshProject(resource, context.getMode()); } + + private void generateCodeForCurrentFederate( + String lfModuleName + ) { + startTimeStepIsPresentCount = 0; + startTimeStepTokens = 0; + code.pr(generateDirectives()); + code.pr(generateTopLevelPreambles()); + code.pr(new CMainGenerator(targetConfig).generateCode()); + // Generate code for each reactor. + generateReactorDefinitions(); + + // Generate main instance, if there is one. + // Note that any main reactors in imported files are ignored. + // Skip generation if there are cycles. + if (main != null) { + initializeTriggerObjects.pr(String.join("\n", + "int _lf_startup_reactions_count = 0;", + "int _lf_shutdown_reactions_count = 0;", + "int _lf_timer_triggers_count = 0;", + "int _lf_tokens_with_ref_count_count = 0;" + )); + + // Create an array of arrays to store all self structs. + // This is needed because connections cannot be established until + // all reactor instances have self structs because ports that + // receive data reference the self structs of the originating + // reactors, which are arbitarily far away in the program graph. + generateSelfStructs(main); + generateReactorInstance(main); + + // If there are timers, create a table of timers to be initialized. + code.pr(CTimerGenerator.generateDeclarations(timerCount)); + + // If there are shutdown reactions, create a table of triggers. + code.pr(CReactionGenerator.generateShutdownTriggersTable(shutdownReactionCount)); + + // If there are modes, create a table of mode state to be checked for transitions. + code.pr(CModesGenerator.generateModeStatesTable( + hasModalReactors, + modalReactorCount, + modalStateResetCount + )); + + // Generate function to return a pointer to the action trigger_t + // that handles incoming network messages destined to the specified + // port. This will only be used if there are federates. + if (currentFederate.networkMessageActions.size() > 0) { + // Create a static array of trigger_t pointers. + // networkMessageActions is a list of Actions, but we + // need a list of trigger struct names for ActionInstances. + // There should be exactly one ActionInstance in the + // main reactor for each Action. + var triggers = new LinkedList(); + for (Action action : currentFederate.networkMessageActions) { + // Find the corresponding ActionInstance. + var actionInstance = main.lookupActionInstance(action); + triggers.add(CUtil.triggerRef(actionInstance, null)); + } + var actionTableCount = 0; + for (String trigger : triggers) { + initializeTriggerObjects.pr("_lf_action_table["+(actionTableCount++)+"] = &"+trigger+";"); + } + code.pr(String.join("\n", + "trigger_t* _lf_action_table["+currentFederate.networkMessageActions.size()+"];", + "trigger_t* _lf_action_for_port(int port_id) {", + " if (port_id < "+currentFederate.networkMessageActions.size()+") {", + " return _lf_action_table[port_id];", + " } else {", + " return NULL;", + " }", + "}" + )); + } else { + code.pr(String.join("\n", + "trigger_t* _lf_action_for_port(int port_id) {", + " return NULL;", + "}" + )); + } + + // Generate function to initialize the trigger objects for all reactors. + code.pr(CTriggerObjectsGenerator.generateInitializeTriggerObjects( + currentFederate, + main, + targetConfig, + initializeTriggerObjects, + startTimeStep, + types, + lfModuleName, + federationRTIProperties, + startTimeStepTokens, + startTimeStepIsPresentCount, + startupReactionCount, + isFederated, + isFederatedAndDecentralized(), + clockSyncIsOn() + )); + + // Generate function to trigger startup reactions for all reactors. + code.pr(CReactionGenerator.generateLfTriggerStartupReactions(startupReactionCount)); + + // Generate function to schedule timers for all reactors. + code.pr(CTimerGenerator.generateLfInitializeTimer(timerCount)); + + // Generate a function that will either do nothing + // (if there is only one federate or the coordination + // is set to decentralized) or, if there are + // downstream federates, will notify the RTI + // that the specified logical time is complete. + code.pr(String.join("\n", + "void logical_tag_complete(tag_t tag_to_send) {", + (isFederatedAndCentralized() ? + " _lf_logical_tag_complete(tag_to_send);" : "" + ), + "}" + )); + + if (isFederated) { + code.pr(CFederateGenerator.generateFederateNeighborStructure(currentFederate).toString()); + } + + // Generate function to schedule shutdown reactions if any + // reactors have reactions to shutdown. + code.pr(CReactionGenerator.generateLfTriggerShutdownReactions( + shutdownReactionCount + )); + + // Generate an empty termination function for non-federated + // execution. For federated execution, an implementation is + // provided in federate.c. That implementation will resign + // from the federation and close any open sockets. + if (!isFederated) { + code.pr("void terminate_execution() {}"); + } + + code.pr(CModesGenerator.generateLfHandleModeChanges( + hasModalReactors, + modalStateResetCount + )); + } + } + + protected DockerGeneratorBase getDockerGenerator() { + return new CDockerGenerator(isFederated, CCppMode, targetConfig); + } @Override public void checkModalReactorSupport(boolean __) { @@ -1195,46 +1098,6 @@ public void createFederatedLauncher() throws IOException{ federationRTIProperties ); } - - /** - * 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 directory where the docker compose file is generated. - * @param The name of the docker file. - * @param The name of the federate. - */ - @Override - public void writeDockerFile( - File dockerComposeDir, - String dockerFileName, - String federateName - ) throws IOException { - if (mainDef == null) { - return; - } - var srcGenPath = fileConfig.getSrcGenPath(); - var dockerFile = srcGenPath + File.separator + dockerFileName; - // If a dockerfile exists, remove it. - var file = new File(dockerFile); - if (file.exists()) { - file.delete(); - } - var contents = new CodeBuilder(); - var compileCommand = IterableExtensions.isNullOrEmpty(targetConfig.buildCommands) ? - CDockerGenerator.generateDefaultCompileCommand() : - joinObjects(targetConfig.buildCommands, " "); - contents.pr(CDockerGenerator.generateDockerFileContent( - topLevelName, - targetConfig.dockerOptions.from, - CCppMode ? "g++" : "gcc", - compileCommand, - srcGenPath) - ); - contents.writeToFile(dockerFile); - System.out.println(getDockerBuildCommand(dockerFile, dockerComposeDir, federateName)); - } protected boolean clockSyncIsOn() { return targetConfig.clockSync != ClockSyncMode.OFF @@ -2347,12 +2210,12 @@ public TargetTypes getTargetTypes() { // // Protected methods. // Perform set up that does not generate code - protected void setUpParameters(LFGeneratorContext context) { + protected void setUpGeneralParameters() { accommodatePhysicalActionsIfPresent(); targetConfig.compileDefinitions.put("LOG_LEVEL", targetConfig.logLevel.ordinal() + ""); targetConfig.compileAdditionalSources.addAll(CCoreFilesUtils.getCTargetSrc()); targetConfig.compileAdditionalSources.add("core" + File.separator + "mixed_radix.c"); - setCSpecificDefaults(context); + setCSpecificDefaults(); // Create the main reactor instance if there is a main reactor. createMainReactorInstance(); // If there are federates, copy the required files for that. @@ -2376,7 +2239,47 @@ protected void setUpParameters(LFGeneratorContext context) { pickScheduler(); } pickCompilePlatform(); - parseTargetParameters(); + } + + // Perform set up that does not generate code + protected void setUpFederateSpecificParameters(FederateInstance federate, CodeBuilder commonCode) { + currentFederate = federate; + if (isFederated) { + // Reset the cmake-includes and files, to be repopulated for each federate individually. + // This is done to enable support for separately + // adding cmake-includes/files for different federates to prevent linking and mixing + // all federates' supporting libraries/files together. + targetConfig.cmakeIncludes.clear(); + targetConfig.cmakeIncludesWithoutPath.clear(); + targetConfig.fileNames.clear(); + targetConfig.filesNamesWithoutPath.clear(); + + // Re-apply the cmake-include target property of the main .lf file. + var target = GeneratorUtils.findTarget(mainDef.getReactorClass().eResource()); + if (target.getConfig() != null) { + // Update the cmake-include + TargetProperty.updateOne( + this.targetConfig, + TargetProperty.CMAKE_INCLUDE, + convertToEmptyListIfNull(target.getConfig().getPairs()), + errorReporter + ); + // Update the files + TargetProperty.updateOne( + this.targetConfig, + TargetProperty.FILES, + convertToEmptyListIfNull(target.getConfig().getPairs()), + errorReporter + ); + } + // Clear out previously generated code. + code = new CodeBuilder(commonCode); + initializeTriggerObjects = new CodeBuilder(); + // Enable clock synchronization if the federate + // is not local and clock-sync is enabled + initializeClockSynchronization(); + startTimeStep = new CodeBuilder(); + } } /** @@ -2616,7 +2519,8 @@ public void enableSupportForSerializationIfApplicable(CancelIndicator cancelIndi * Generate code that needs to appear at the top of the generated * C file, such as #define and #include statements. */ - public void generateDirectives() { + public String generateDirectives() { + CodeBuilder code = new CodeBuilder(); code.prComment("Code generated by the Lingua Franca compiler from:"); code.prComment("file:/" + FileUtil.toUnixString(fileConfig.srcFile)); code.pr(CPreambleGenerator.generateDefineDirectives( @@ -2627,56 +2531,25 @@ public void generateDirectives() { clockSyncIsOn(), hasModalReactors )); - code.pr(CPreambleGenerator.generateIncludeStatements( targetConfig, isFederated )); + return code.toString(); } /** * Generate top-level preamble code. */ - protected void generateTopLevelPreambles() { + protected String generateTopLevelPreambles() { + CodeBuilder code = new CodeBuilder(); if (this.mainDef != null) { var mainModel = (Model) toDefinition(mainDef.getReactorClass()).eContainer(); for (Preamble p : mainModel.getPreambles()) { code.pr(toText(p.getCode())); } } - } - - /** - * Parse the target parameters and set flags to the runCommand - * accordingly. - */ - private void parseTargetParameters() { - if (targetConfig.fastMode) { - // The runCommand has a first entry that is ignored but needed. - if (runCommand.size() == 0) { - runCommand.add(topLevelName); - } - runCommand.add("-f"); - runCommand.add("true"); - } - if (targetConfig.keepalive) { - // The runCommand has a first entry that is ignored but needed. - if (runCommand.size() == 0) { - runCommand.add(topLevelName); - } - runCommand.add("-k"); - runCommand.add("true"); - } - if (targetConfig.timeout != null) { - // The runCommand has a first entry that is ignored but needed. - if (runCommand.size() == 0) { - runCommand.add(topLevelName); - } - runCommand.add("-o"); - runCommand.add(targetConfig.timeout.getMagnitude() + ""); - runCommand.add(targetConfig.timeout.unit.getCanonicalName()); - } - + return code.toString(); } /** Given a line of text from the output of a compiler, return diff --git a/org.lflang/src/org/lflang/generator/c/CMainGenerator.java b/org.lflang/src/org/lflang/generator/c/CMainGenerator.java index 5d955f9841..1dec6550d2 100644 --- a/org.lflang/src/org/lflang/generator/c/CMainGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CMainGenerator.java @@ -1,11 +1,90 @@ package org.lflang.generator.c; +import java.util.ArrayList; +import java.util.List; +import org.lflang.TargetConfig; +import org.lflang.generator.CodeBuilder; +import org.lflang.util.StringUtil; + public class CMainGenerator { - public static String generateCode() { + private TargetConfig targetConfig; + /** The command to run the generated code if specified in the target directive. */ + private List runCommand; + + public CMainGenerator(TargetConfig targetConfig) { + this.targetConfig = targetConfig; + runCommand = new ArrayList<>(); + parseTargetParameters(); + } + + /** + * Generate the code that is the entry point + * of the program. + * + * Ideally, this code would belong to its own `main.c` + * file, but it currently lives in the same file + * as all the code generated for reactors. + */ + public String generateCode() { + CodeBuilder code = new CodeBuilder(); + code.pr(generateMainFunction()); + code.pr(generateSetDefaultCliOption()); + return code.toString(); + } + + /** + * Generate the `main` function. + */ + private String generateMainFunction() { return String.join("\n", "int main(int argc, char* argv[]) {", " return lf_reactor_c_main(argc, argv);", "}" ); } + + /** + * Generate code that is used to override the + * command line options to the `main` function + */ + private String generateSetDefaultCliOption() { + // Generate function to set default command-line options. + // A literal array needs to be given outside any function definition, + // so start with that. + return runCommand.size() > 0 ? + String.join("\n", + "char* _lf_default_argv[] = { " + + StringUtil.addDoubleQuotes( + StringUtil.joinObjects(runCommand, + StringUtil.addDoubleQuotes(", ")))+" };", + "void _lf_set_default_command_line_options() {", + " default_argc = "+runCommand.size()+";", + " default_argv = _lf_default_argv;", + "}") + : "void _lf_set_default_command_line_options() {}"; + } + + /** + * Parse the target parameters and set flags to the runCommand + * accordingly. + */ + private void parseTargetParameters() { + if (targetConfig.fastMode) { + runCommand.add("-f"); + runCommand.add("true"); + } + if (targetConfig.keepalive) { + runCommand.add("-k"); + runCommand.add("true"); + } + if (targetConfig.timeout != null) { + runCommand.add("-o"); + runCommand.add(targetConfig.timeout.getMagnitude() + ""); + runCommand.add(targetConfig.timeout.unit.getCanonicalName()); + } + // The runCommand has a first entry that is ignored but needed. + if (runCommand.size() > 0) { + runCommand.add(0, "dummy"); + } + } } diff --git a/org.lflang/src/org/lflang/generator/c/CTriggerObjectsGenerator.java b/org.lflang/src/org/lflang/generator/c/CTriggerObjectsGenerator.java index 5c6a9fc0d4..a22628a388 100644 --- a/org.lflang/src/org/lflang/generator/c/CTriggerObjectsGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CTriggerObjectsGenerator.java @@ -43,7 +43,7 @@ public static String generateInitializeTriggerObjects( CodeBuilder initializeTriggerObjects, CodeBuilder startTimeStep, CTypes types, - String topLevelName, + String lfModuleName, LinkedHashMap federationRTIProperties, int startTimeStepTokens, int startTimeStepIsPresentCount, @@ -63,7 +63,7 @@ public static String generateInitializeTriggerObjects( // Initialize tracing if it is enabled if (targetConfig.tracing != null) { - var traceFileName = topLevelName; + var traceFileName = lfModuleName; if (targetConfig.tracing.traceFileName != null) { traceFileName = targetConfig.tracing.traceFileName; // Since all federates would have the same name, we need to append the federate name. diff --git a/org.lflang/src/org/lflang/generator/python/PythonDockerGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonDockerGenerator.java index f364258d6b..f256e4dd5f 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonDockerGenerator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonDockerGenerator.java @@ -1,18 +1,39 @@ package org.lflang.generator.python; +import org.lflang.TargetConfig; +import org.lflang.generator.DockerGeneratorBase; -import java.nio.file.Path; +/** + * Generates the docker file related code for the Python target. + * + * @author{Hou Seng Wong } + */ +public class PythonDockerGenerator extends DockerGeneratorBase { + TargetConfig targetConfig; + final String defaultBaseImage = "python:slim"; -public class PythonDockerGenerator { - public static String generateDockerFileContent(String topLevelName, Path srcGenPath) { - return String.join("\n", - "# Generated docker file for "+topLevelName+".lf in "+srcGenPath+".", + public PythonDockerGenerator(boolean isFederated, TargetConfig targetConfig) { + super(isFederated); + this.targetConfig = targetConfig; + } + + /** + * Generates the contents of the docker file. + * + * @param lfModuleName The name of the lingua franca module. + * In unfederated execution, this is fileConfig.name. + * In federated execution, this is typically fileConfig.name + "_" + federate.name + */ + @Override + public String generateDockerFileContent(String lfModuleName) { + var baseImage = defaultBaseImage; + return String.join("\n", "# For instructions, see: https://github.com/icyphy/lingua-franca/wiki/Containerized-Execution", - "FROM python:slim", - "WORKDIR /lingua-franca/"+topLevelName, + "FROM "+baseImage, + "WORKDIR /lingua-franca/"+lfModuleName, "RUN set -ex && apt-get update && apt-get install -y python3-pip", "COPY . src-gen", "RUN cd src-gen && python3 setup.py install && cd ..", - "ENTRYPOINT [\"python3\", \"src-gen/"+topLevelName+".py\"]" + "ENTRYPOINT [\"python3\", \"src-gen/"+lfModuleName+".py\"]" ); } } diff --git a/org.lflang/src/org/lflang/generator/python/PythonGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonGenerator.java index c42e87488e..bc3eb0f791 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonGenerator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonGenerator.java @@ -57,6 +57,7 @@ import org.lflang.federated.serialization.SupportedSerializers; import org.lflang.generator.CodeBuilder; import org.lflang.generator.CodeMap; +import org.lflang.generator.DockerGeneratorBase; import org.lflang.generator.GeneratorResult; import org.lflang.generator.GeneratorUtils; import org.lflang.generator.IntegratedBuilder; @@ -193,21 +194,21 @@ public String generatePythonReactorClasses(FederateInstance federate) { * Generate the Python code constructed from reactor classes and user-written classes. * @return the code body */ - public String generatePythonCode(FederateInstance federate) { + public String generatePythonCode(FederateInstance federate, String pyModuleName) { return String.join("\n", "import os", "import sys", "sys.path.append(os.path.dirname(__file__))", "# List imported names, but do not use pylint's --extension-pkg-allow-list option", "# so that these names will be assumed present without having to compile and install.", - "from LinguaFranca"+topLevelName+" import ( # pylint: disable=no-name-in-module, import-error", + "from "+pyModuleName+" import ( # pylint: disable=no-name-in-module, import-error", " Tag, action_capsule_t, compare_tags, get_current_tag, get_elapsed_logical_time,", " get_elapsed_physical_time, get_logical_time, get_microstep, get_physical_time,", " get_start_time, port_capsule, request_stop, schedule_copy,", " start", ")", "# pylint: disable=c-extension-no-member", - "import LinguaFranca"+topLevelName+" as lf", + "import "+pyModuleName+" as lf", "try:", " from LinguaFrancaBase.constants import BILLION, FOREVER, NEVER, instant_t, interval_t", " from LinguaFrancaBase.functions import (", @@ -237,11 +238,9 @@ public String generatePythonCode(FederateInstance federate) { * If the LF program itself is threaded or if tracing is enabled, NUMBER_OF_WORKERS is added as a macro * so that platform-specific C files will contain the appropriate functions. */ - public String generatePythonSetupFile() { - String moduleName = "LinguaFranca" + topLevelName; - + public String generatePythonSetupFile(String lfModuleName, String pyModuleName) { List sources = new ArrayList<>(targetConfig.compileAdditionalSources); - sources.add(topLevelName + ".c"); + sources.add(lfModuleName + ".c"); sources = sources.stream() .map(Paths::get) .map(FileUtil::toUnixString) @@ -249,7 +248,7 @@ public String generatePythonSetupFile() { .collect(Collectors.toList()); List macros = new ArrayList<>(); - macros.add(generateMacroEntry("MODULE_NAME", moduleName)); + macros.add(generateMacroEntry("MODULE_NAME", pyModuleName)); for (var entry : targetConfig.compileDefinitions.entrySet()) { macros.add(generateMacroEntry(entry.getKey(), entry.getValue())); @@ -266,12 +265,12 @@ public String generatePythonSetupFile() { return String.join("\n", "from setuptools import setup, Extension", "", - "linguafranca"+topLevelName+"module = Extension("+StringUtil.addDoubleQuotes(moduleName)+",", + "linguafranca"+lfModuleName+"module = Extension("+StringUtil.addDoubleQuotes(pyModuleName)+",", " sources = ["+String.join(", ", sources)+"],", " define_macros=["+String.join(", ", macros)+"])", "", - "setup(name="+StringUtil.addDoubleQuotes(moduleName)+", version=\"1.0\",", - " ext_modules = [linguafranca"+topLevelName+"module],", + "setup(name="+StringUtil.addDoubleQuotes(pyModuleName)+", version=\"1.0\",", + " ext_modules = [linguafranca"+lfModuleName+"module],", " install_requires=["+String.join(", ", installRequires)+"])" ); } @@ -280,8 +279,13 @@ public String generatePythonSetupFile() { * Generate the necessary Python files. * @param federate The federate instance */ - public Map generatePythonFiles(FederateInstance federate) throws IOException { - Path filePath = fileConfig.getSrcGenPath().resolve(topLevelName + ".py"); + public Map generatePythonFiles( + FederateInstance federate, + String lfModuleName, + String pyModuleName, + String pyFileName + ) throws IOException { + Path filePath = fileConfig.getSrcGenPath().resolve(pyFileName); File file = filePath.toFile(); Files.deleteIfExists(filePath); // Create the necessary directories @@ -289,7 +293,8 @@ public Map generatePythonFiles(FederateInstance federate) throws file.getParentFile().mkdirs(); } Map codeMaps = new HashMap<>(); - codeMaps.put(filePath, CodeMap.fromGeneratedCode(generatePythonCode(federate).toString())); + codeMaps.put(filePath, CodeMap.fromGeneratedCode( + generatePythonCode(federate, pyModuleName).toString())); FileUtil.writeToFile(codeMaps.get(filePath).getGeneratedCode(), filePath); Path setupPath = fileConfig.getSrcGenPath().resolve("setup.py"); @@ -298,7 +303,7 @@ public Map generatePythonFiles(FederateInstance federate) throws Files.deleteIfExists(setupPath); // Create the setup file - FileUtil.writeToFile(generatePythonSetupFile(), setupPath); + FileUtil.writeToFile(generatePythonSetupFile(lfModuleName, pyModuleName), setupPath); return codeMaps; } @@ -337,7 +342,8 @@ public void pythonCompileCode(LFGeneratorContext context) { * C file, such as #define and #include statements. */ @Override - public void generateDirectives() { + public String generateDirectives() { + CodeBuilder code = new CodeBuilder(); code.prComment("Code generated by the Lingua Franca compiler from:"); code.prComment("file:/" + FileUtil.toUnixString(fileConfig.srcFile)); code.pr(PythonPreambleGenerator.generateCDefineDirectives( @@ -345,13 +351,15 @@ public void generateDirectives() { fileConfig.getSrcGenPath(), clockSyncIsOn(), hasModalReactors)); code.pr(PythonPreambleGenerator.generateCIncludeStatements( targetConfig, isFederated, hasModalReactors)); + return code.toString(); } /** * Override generate top-level preambles, but put the preambles in the * .py file rather than the C file. */ - protected void generateTopLevelPreambles() { + @Override + protected String generateTopLevelPreambles() { Set models = new LinkedHashSet<>(); for (Reactor r : ASTUtils.convertToEmptyListIfNull(reactors)) { // The following assumes all reactors have a container. @@ -366,6 +374,7 @@ protected void generateTopLevelPreambles() { for (Model m : models) { pythonPreamble.pr(PythonPreambleGenerator.generatePythonPreambles(m.getPreambles())); } + return ""; } /** @@ -629,15 +638,14 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { return; } - String baseFileName = topLevelName; // Keep a separate file config for each federate FileConfig oldFileConfig = fileConfig; var federateCount = 0; Map codeMaps = new HashMap<>(); for (FederateInstance federate : federates) { federateCount++; + var lfModuleName = isFederated ? fileConfig.name + "_" + federate.name : fileConfig.name; if (isFederated) { - topLevelName = baseFileName + '_' + federate.name; try { fileConfig = new FedFileConfig(fileConfig, federate.name); } catch (IOException e) { @@ -647,7 +655,7 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { // Don't generate code if there is no main reactor if (this.main != null) { try { - Map codeMapsForFederate = generatePythonFiles(federate); + Map codeMapsForFederate = generatePythonFiles(federate, lfModuleName, generatePythonModuleName(lfModuleName), generatePythonFileName(lfModuleName)); codeMaps.putAll(codeMapsForFederate); copyTargetFiles(); if (!targetConfig.noCompile) { @@ -673,7 +681,7 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { } if (!isFederated) { - System.out.println(PythonInfoGenerator.generateRunInfo(fileConfig, topLevelName)); + System.out.println(PythonInfoGenerator.generateRunInfo(fileConfig, lfModuleName)); } } fileConfig = oldFileConfig; @@ -681,12 +689,11 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { if (isFederated) { System.out.println(PythonInfoGenerator.generateFedRunInfo(fileConfig)); } - // Restore filename - topLevelName = baseFileName; + if (errorReporter.getErrorsOccurred()) { context.unsuccessfulFinish(); } else if (!isFederated) { - context.finish(GeneratorResult.Status.COMPILED, topLevelName+".py", fileConfig.getSrcGenPath(), fileConfig, + context.finish(GeneratorResult.Status.COMPILED, fileConfig.name+".py", fileConfig.getSrcGenPath(), fileConfig, codeMaps, "python3"); } else { context.finish(GeneratorResult.Status.COMPILED, fileConfig.name, fileConfig.binPath, fileConfig, codeMaps, @@ -694,6 +701,10 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { } } + @Override + protected DockerGeneratorBase getDockerGenerator() { + return new PythonDockerGenerator(isFederated, targetConfig); + } /** * Generate code for the body of a reaction that takes an input and @@ -770,7 +781,7 @@ public void generateParameterInitialization(ReactorInstance instance) { } /** - * Generates C preambles defined by user for a given reactor + * Generate C preambles defined by user for a given reactor * Since the Python generator expects preambles written in C, * this function is overridden and does nothing. * @param reactor The given reactor @@ -786,11 +797,11 @@ public void generateUserPreamblesForReactor(Reactor reactor) { * @param instance The reactor instance. * @param reactions The reactions of this instance. */ - @Override + @Override public void generateReactorInstanceExtension( ReactorInstance instance ) { - initializeTriggerObjects.pr(PythonReactionGenerator.generateCPythonReactionLinkers(instance, mainDef, topLevelName)); + initializeTriggerObjects.pr(PythonReactionGenerator.generateCPythonReactionLinkers(instance, mainDef)); } /** @@ -819,30 +830,6 @@ public void generateSelfStructExtension( reactionIndex++; } } - - /** - * 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 directory where the docker compose file is generated. - * @param The name of the docker file. - * @param The name of the federate. - */ - @Override - public void writeDockerFile(File dockerComposeDir, String dockerFileName, String federateName) throws IOException { - if (mainDef == null) { - return; - } - Path srcGenPath = fileConfig.getSrcGenPath(); - String dockerFile = srcGenPath + File.separator + dockerFileName; - CodeBuilder contents = new CodeBuilder(); - contents.pr(PythonDockerGenerator.generateDockerFileContent(topLevelName, srcGenPath)); - // If a dockerfile exists, remove it. - Files.deleteIfExists(srcGenPath.resolve(dockerFileName)); - contents.writeToFile(dockerFile); - System.out.println(getDockerBuildCommand(dockerFile, dockerComposeDir, federateName)); - } @Override protected String getConflictingConnectionsInModalReactorsBody(String source, String dest) { @@ -856,8 +843,8 @@ protected String getConflictingConnectionsInModalReactorsBody(String source, Str } @Override - protected void setUpParameters(LFGeneratorContext context) { - super.setUpParameters(context); + protected void setUpGeneralParameters() { + super.setUpGeneralParameters(); if (hasModalReactors) { targetConfig.compileAdditionalSources.add("modal_models/impl.c"); } @@ -871,10 +858,44 @@ protected void generateStartupReactionsInModesIfNeeded() { PythonModeGenerator.generateStartupReactionsInModesIfNeeded(reactors); } + /** + * Generate a (`key`, `val`) tuple pair for the `define_macros` field + * of the Extension class constructor from setuptools. + * + * @param key The key of the macro entry + * @param val The value of the macro entry + * @return A (`key`, `val`) tuple pair as String + */ private static String generateMacroEntry(String key, String val) { return "(" + StringUtil.addDoubleQuotes(key) + ", " + StringUtil.addDoubleQuotes(val) + ")"; } + /** + * Generate the name of the python module. + * + * Ideally, this function would belong in a class like `PyFileConfig` + * that specifies all the paths to the generated code. + * + * @param lfModuleName The name of the LF module. + * @return The name of the python module. + */ + private static String generatePythonModuleName(String lfModuleName) { + return "LinguaFranca" + lfModuleName; + } + + /** + * Generate the python file name given an `lfModuleName`. + * + * Ideally, this function would belong in a class like `PyFileConfig` + * that specifies all the paths to the generated code. + * + * @param lfModuleName The name of the LF module + * @return The name of the generated python file. + */ + private static String generatePythonFileName(String lfModuleName) { + return lfModuleName + ".py"; + } + /** * Copy Python specific target code to the src-gen directory */ diff --git a/org.lflang/src/org/lflang/generator/python/PythonInfoGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonInfoGenerator.java index 9f61f2021a..a7b0e88ec6 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonInfoGenerator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonInfoGenerator.java @@ -25,13 +25,13 @@ public static String generateSetupInfo(FileConfig fileConfig) { /** * Print information on how to execute the generated program. */ - public static String generateRunInfo(FileConfig fileConfig, String topLevelName) { + public static String generateRunInfo(FileConfig fileConfig, String lfModuleName) { return String.join("\n", "", "#####################################", "To run the generated program, use:", " ", - " python3 "+fileConfig.getSrcGenPath()+File.separator+topLevelName+".py", + " python3 "+fileConfig.getSrcGenPath()+File.separator+lfModuleName+".py", "", "#####################################", "" diff --git a/org.lflang/src/org/lflang/generator/python/PythonReactionGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonReactionGenerator.java index 16d7758cb3..756997e908 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonReactionGenerator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonReactionGenerator.java @@ -382,12 +382,10 @@ public static String generateCDelayBody(Action action, VarRef port, boolean isTo * @param instance The reactor instance. * @param reactions The reactions of this instance. * @param mainDef The definition of the main reactor - * @param topLevelName The name of the module */ public static String generateCPythonReactionLinkers( ReactorInstance instance, - Instantiation mainDef, - String topLevelName + Instantiation mainDef ) { String nameOfSelfStruct = CUtil.reactorRef(instance); Reactor reactor = ASTUtils.toDefinition(instance.getDefinition().getReactorClass()); @@ -405,7 +403,7 @@ public static String generateCPythonReactionLinkers( for (ReactionInstance reaction : instance.reactions) { // Create a PyObject for each reaction - code.pr(generateCPythonReactionLinker(instance, reaction, topLevelName, nameOfSelfStruct)); + code.pr(generateCPythonReactionLinker(instance, reaction, nameOfSelfStruct)); } return code.toString(); } @@ -414,13 +412,11 @@ public static String generateCPythonReactionLinkers( * Generate Python code to link cpython functions to python functions for a reaction. * @param instance The reactor instance. * @param reaction The reaction of this instance to link. - * @param topLevelName The name of the module. * @param nameOfSelfStruct The name of the self struct in cpython. */ public static String generateCPythonReactionLinker( ReactorInstance instance, ReactionInstance reaction, - String topLevelName, String nameOfSelfStruct ) { CodeBuilder code = new CodeBuilder(); diff --git a/org.lflang/src/org/lflang/generator/ts/TSDockerGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSDockerGenerator.kt index 6f18086e59..0dbe649dd9 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSDockerGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSDockerGenerator.kt @@ -1,17 +1,19 @@ package org.lflang.generator.ts -import java.util.StringJoiner +import org.lflang.generator.DockerGeneratorBase; /** - * Generate parameters for TypeScript target. + * Generates the docker file related code for the Typescript target. + * + * @author{Hou Seng Wong } */ -class TSDockerGenerator ( +class TSDockerGenerator( private val tsFileName: String - ) { + ) : DockerGeneratorBase(false) { /** * Returns the content of the docker file for [tsFileName]. */ - fun generateDockerFileContent(): String { + override fun generateDockerFileContent(lfModuleName: String): String { val dockerFileContent = """ |FROM node:alpine |WORKDIR /linguafranca/$tsFileName @@ -20,22 +22,4 @@ class TSDockerGenerator ( """ return dockerFileContent.trimMargin() } - - /** - * Returns the content of the docker compose file for [tsFileName]. - */ - fun generateDockerComposeFileContent(): String { - val dockerComposeFileContent = """ - |version: "3.9" - |services: - | ${tsFileName.toLowerCase()}: - | build: - | context: . - | dockerfile: HelloWorldContainerized.Dockerfile - |networks: - | default: - | name: lf - """ - return dockerComposeFileContent.trimMargin() - } -} \ No newline at end of file +} diff --git a/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt b/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt index fd844bf912..7549d2c78b 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt @@ -62,4 +62,18 @@ class TSFileConfig( * Path to TypeScript core source code. */ fun tsCoreGenPath(): Path = tsSrcGenPath().resolve("core") + + /** + * Path to the generated docker file + */ + fun tsDockerFilePath(tsFileName: String): Path { + return srcGenPath.resolve(tsFileName + ".Dockerfile") + } + + /** + * Path to the generated docker compose file + */ + fun tsDockerComposeFilePath(): Path { + return srcGenPath.resolve("docker-compose.yml") + } } \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt index 3b85cde150..a62e01bb2c 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt @@ -148,14 +148,14 @@ class TSGenerator( if (!canGenerate(errorsOccurred(), mainDef, errorReporter, context)) return if (!isOsCompatible()) return - + // FIXME: The following operation must be done after levels are assigned. // Removing these ports before that will cause incorrect levels to be assigned. // See https://github.com/lf-lang/lingua-franca/discussions/608 // For now, avoid compile errors by removing disconnected network ports before // assigning levels. removeRemoteFederateConnectionPorts(null); - + clean(context) copyRuntime() copyConfigFiles() @@ -275,15 +275,12 @@ class TSGenerator( if (targetConfig.dockerOptions != null && isFederated) { println("WARNING: Federated Docker file generation is not supported on the Typescript target. No docker file is generated.") } else if (targetConfig.dockerOptions != null) { - val dockerFilePath = fileConfig.srcGenPath.resolve("$tsFileName.Dockerfile") - val dockerComposeFile = fileConfig.srcGenPath.resolve("docker-compose.yml") val dockerGenerator = TSDockerGenerator(tsFileName) - println("docker file written to $dockerFilePath") - FileUtil.writeToFile(dockerGenerator.generateDockerFileContent(), dockerFilePath) - FileUtil.writeToFile( - dockerGenerator.generateDockerComposeFileContent(), - dockerComposeFile - ) + dockerGenerator.addFederate( + tsFileName, tsFileName, + tsFileConfig.tsDockerFilePath(tsFileName), + targetConfig) + dockerGenerator.writeDockerFiles(tsFileConfig.tsDockerComposeFilePath()) } }