diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java index 05a5f59bfd..ae93a44056 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/LinguaFrancaSynthesis.java @@ -118,6 +118,7 @@ import org.lflang.lf.Connection; import org.lflang.lf.Model; import org.lflang.lf.Reactor; +import org.lflang.util.FileUtil; /** * Diagram synthesis for Lingua Franca programs. @@ -985,7 +986,7 @@ private String createReactorLabel(ReactorInstance reactorInstance) { } if (reactorInstance.isMainOrFederated()) { try { - b.append(FileConfig.nameWithoutExtension(reactorInstance.reactorDeclaration.eResource())); + b.append(FileUtil.nameWithoutExtension(reactorInstance.reactorDeclaration.eResource())); } catch (Exception e) { throw Exceptions.sneakyThrow(e); } diff --git a/org.lflang.lfc/src/org/lflang/lfc/Main.java b/org.lflang.lfc/src/org/lflang/lfc/Main.java index 829a854204..6081892a42 100644 --- a/org.lflang.lfc/src/org/lflang/lfc/Main.java +++ b/org.lflang.lfc/src/org/lflang/lfc/Main.java @@ -35,9 +35,9 @@ import org.lflang.FileConfig; import org.lflang.LFRuntimeModule; import org.lflang.LFStandaloneSetup; -import org.lflang.TargetConfig.Mode; import org.lflang.generator.LFGeneratorContext; import org.lflang.generator.MainContext; +import org.lflang.util.FileUtil; import com.google.inject.Inject; import com.google.inject.Injector; @@ -285,7 +285,7 @@ private void runGenerator(List files, Injector injector) { exitIfCollectedErrors(); LFGeneratorContext context = new MainContext( - Mode.STANDALONE,CancelIndicator.NullImpl, (m, p) -> {}, properties, false, + LFGeneratorContext.Mode.STANDALONE, CancelIndicator.NullImpl, (m, p) -> {}, properties, false, fileConfig -> injector.getInstance(ErrorReporter.class) ); @@ -346,7 +346,7 @@ public Resource getValidatedResource(Path path) { issueCollector.accept(new LfIssue(issue.getMessage(), issue.getSeverity(), issue.getLineNumber(), issue.getColumn(), issue.getLineNumberEnd(), issue.getColumnEnd(), - issue.getLength(), FileConfig.toPath(uri))); + issue.getLength(), FileUtil.toPath(uri))); } catch (IOException e) { reporter.printError("Unable to convert '" + uri + "' to path." + e); } diff --git a/org.lflang.lfc/src/org/lflang/lfc/StandaloneIssueAcceptor.java b/org.lflang.lfc/src/org/lflang/lfc/StandaloneIssueAcceptor.java index 14d81d72bc..3e5e3965eb 100644 --- a/org.lflang.lfc/src/org/lflang/lfc/StandaloneIssueAcceptor.java +++ b/org.lflang.lfc/src/org/lflang/lfc/StandaloneIssueAcceptor.java @@ -9,7 +9,7 @@ import org.eclipse.xtext.validation.EObjectDiagnosticImpl; import org.eclipse.xtext.validation.ValidationMessageAcceptor; -import org.lflang.FileConfig; +import org.lflang.util.FileUtil; import com.google.inject.Inject; @@ -57,7 +57,7 @@ void accept(Severity severity, String message, EObject object, EStructuralFeatur private Path getPath(EObjectDiagnosticImpl diagnostic) { Path file = null; try { - file = FileConfig.toPath(diagnostic.getUriToProblem()); + file = FileUtil.toPath(diagnostic.getUriToProblem()); } catch (IOException e) { // just continue with null } diff --git a/org.lflang.tests/src/org/lflang/tests/Configurators.java b/org.lflang.tests/src/org/lflang/tests/Configurators.java index 1e64dc5f5e..ec2521ab77 100644 --- a/org.lflang.tests/src/org/lflang/tests/Configurators.java +++ b/org.lflang.tests/src/org/lflang/tests/Configurators.java @@ -52,7 +52,7 @@ public interface Configurator { * @return True if successful, false otherwise. */ public static boolean useSingleThread(LFTest test) { - test.getContext().getArgs().setProperty("threads", "0"); + test.context.getArgs().setProperty("threads", "0"); return true; } @@ -63,7 +63,7 @@ public static boolean useSingleThread(LFTest test) { * @return True if successful, false otherwise. */ public static boolean useFourThreads(LFTest test) { - test.getContext().getArgs().setProperty("threads", "4"); + test.context.getArgs().setProperty("threads", "4"); return true; } diff --git a/org.lflang.tests/src/org/lflang/tests/LFTest.java b/org.lflang.tests/src/org/lflang/tests/LFTest.java index 64a6691fca..ba096315ee 100644 --- a/org.lflang.tests/src/org/lflang/tests/LFTest.java +++ b/org.lflang.tests/src/org/lflang/tests/LFTest.java @@ -35,6 +35,9 @@ public class LFTest implements Comparable { /** Object used to determine where the code generator puts files. */ public FileConfig fileConfig; + /** Context provided to the code generators */ + public LFGeneratorContext context; + /** Path of the test program relative to the package root. */ private final Path relativePath; @@ -113,14 +116,6 @@ public boolean hasFailed() { return result != Result.TEST_PASS; } - /** - * Return the context stored in this test's file configuration. - * @return The context for this test, to be passed to the code generator. - */ - public LFGeneratorContext getContext() { - return this.fileConfig.context; - } - /** * Compile a string that contains all collected errors and return it. * @return A string that contains all collected errors. diff --git a/org.lflang.tests/src/org/lflang/tests/TestBase.java b/org.lflang.tests/src/org/lflang/tests/TestBase.java index 46374aaaf4..641f35ff7d 100644 --- a/org.lflang.tests/src/org/lflang/tests/TestBase.java +++ b/org.lflang.tests/src/org/lflang/tests/TestBase.java @@ -45,7 +45,6 @@ import org.lflang.LFRuntimeModule; import org.lflang.LFStandaloneSetup; import org.lflang.Target; -import org.lflang.TargetConfig.Mode; import org.lflang.generator.GeneratorResult; import org.lflang.generator.LFGenerator; import org.lflang.generator.LFGeneratorContext; @@ -366,7 +365,7 @@ private static void checkAndReportFailures(Set tests) { */ private LFGeneratorContext configure(LFTest test, Configurator configurator, TestLevel level) throws IOException { var context = new MainContext( - Mode.STANDALONE, CancelIndicator.NullImpl, (m, p) -> {}, new Properties(), true, + LFGeneratorContext.Mode.STANDALONE, CancelIndicator.NullImpl, (m, p) -> {}, new Properties(), true, fileConfig -> new DefaultErrorReporter() ); @@ -380,7 +379,8 @@ private LFGeneratorContext configure(LFTest test, Configurator configurator, Tes } fileAccess.setOutputPath(FileConfig.findPackageRoot(test.srcFile, s -> {}).resolve(FileConfig.DEFAULT_SRC_GEN_DIR).toString()); - test.fileConfig = new FileConfig(r, FileConfig.getSrcGenRoot(fileAccess), context); + test.context = context; + test.fileConfig = new FileConfig(r, FileConfig.getSrcGenRoot(fileAccess), context.useHierarchicalBin()); // Set the no-compile flag the test is not supposed to reach the build stage. if (level.compareTo(TestLevel.BUILD) < 0) { @@ -437,8 +437,8 @@ protected void addExtraLfcArgs(Properties args) { private GeneratorResult generateCode(LFTest test) { GeneratorResult result = GeneratorResult.NOTHING; if (test.fileConfig.resource != null) { - generator.doGenerate(test.fileConfig.resource, fileAccess, test.fileConfig.context); - result = test.fileConfig.context.getResult(); + generator.doGenerate(test.fileConfig.resource, fileAccess, test.context); + result = test.context.getResult(); if (generator.errorsOccurred()) { test.result = Result.CODE_GEN_FAIL; throw new AssertionError("Code generation unsuccessful."); diff --git a/org.lflang.tests/src/org/lflang/tests/runtime/CSchedulerTest.java b/org.lflang.tests/src/org/lflang/tests/runtime/CSchedulerTest.java index c4fa0a2b2b..d9ab56ed30 100644 --- a/org.lflang.tests/src/org/lflang/tests/runtime/CSchedulerTest.java +++ b/org.lflang.tests/src/org/lflang/tests/runtime/CSchedulerTest.java @@ -59,8 +59,7 @@ private void runTest(SchedulerOption scheduler, EnumSet categories Message.DESC_SCHED_SWAPPING + scheduler.toString() +".", categories::contains, test -> { - test.getContext() - .getArgs() + test.context.getArgs() .setProperty( "scheduler", scheduler.toString() diff --git a/org.lflang/src/org/lflang/FileConfig.java b/org.lflang/src/org/lflang/FileConfig.java index 234b094581..3361a91696 100644 --- a/org.lflang/src/org/lflang/FileConfig.java +++ b/org.lflang/src/org/lflang/FileConfig.java @@ -1,43 +1,18 @@ package org.lflang; -import java.io.BufferedInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.net.JarURLConnection; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLConnection; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Enumeration; -import java.util.LinkedList; -import java.util.List; import java.util.function.Consumer; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; -import org.eclipse.core.runtime.FileLocator; -import org.eclipse.core.runtime.IPath; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.xtext.generator.IFileSystemAccess2; -import org.eclipse.xtext.util.RuntimeIOException; -import org.lflang.generator.LFGeneratorContext; -import org.lflang.lf.Reactor; +import org.lflang.util.FileUtil; /** * Base class that governs the interactions between code generators and the file system. @@ -61,19 +36,6 @@ public class FileConfig { */ public static final String DEFAULT_SRC_GEN_DIR = "src-gen"; - /** - * Suffix that when appended to the name of a federated reactor yields - * the name of its corresponding RTI executable. - */ - public static final String RTI_BIN_SUFFIX = "_RTI"; - - /** - * Suffix that when appended to the name of a federated reactor yields - * the name of its corresponding distribution script. - */ - public static final String RTI_DISTRIBUTION_SCRIPT_SUFFIX = "_distribute.sh"; - - // Public fields. /** @@ -81,14 +43,6 @@ public class FileConfig { */ public final Path binPath; - /** - * Object used for communication between the IDE or stand-alone compiler - * and the code generator. - */ - // FIXME: Delete this field? It is used, but it seems out of place, especially given that many methods where a - // FileConfig is used also have access to the context (or their callers have access to the context) - public final LFGeneratorContext context; - /** * The name of the main reactor, which has to match the file name (without * the '.lf' extension). @@ -112,14 +66,6 @@ public class FileConfig { */ public final Resource resource; - /** - * If running in an Eclipse IDE, the iResource refers to the - * IFile representing the Lingua Franca program. - * This is the XText view of the file, which is distinct - * from the Eclipse eCore view of the file and the OS view of the file. - */ - public final IResource iResource; - /** * The full path to the file containing the .lf file including the * full filename with the .lf extension. @@ -131,6 +77,11 @@ public class FileConfig { */ public final Path srcPath; + /** + * Indicate whether the bin directory should be hierarchical. + */ + public final boolean useHierarchicalBin; + // Protected fields. /** @@ -146,7 +97,8 @@ public class FileConfig { * to the package root, then the generated sources will be put in x/y/Z * relative to srcGenBasePath. */ - private Path srcGenPath; + protected Path srcGenPath; + // private fields @@ -169,124 +121,30 @@ public class FileConfig { */ private final Path srcGenPkgPath; - - public FileConfig(Resource resource, Path srcGenBasePath, LFGeneratorContext context) throws IOException { + public FileConfig(Resource resource, Path srcGenBasePath, boolean useHierarchicalBin) throws IOException { this.resource = resource; - this.context = context; + this.useHierarchicalBin = useHierarchicalBin; - this.srcFile = toPath(this.resource); + this.srcFile = FileUtil.toPath(this.resource); this.srcPath = srcFile.getParent(); this.srcPkgPath = getPkgPath(resource); this.srcGenBasePath = srcGenBasePath; - this.name = nameWithoutExtension(this.srcFile); - this.srcGenPath = getSrcGenPath(this.srcGenBasePath, this.srcPkgPath, - this.srcPath, name); + this.name = FileUtil.nameWithoutExtension(this.srcFile); + this.srcGenPath = srcGenBasePath.resolve(getSubPkgPath(srcPath)).resolve(name); this.srcGenPkgPath = this.srcGenPath; this.outPath = srcGenBasePath.getParent(); - this.binPath = getBinPath(this.srcPkgPath, this.srcPath, this.outPath, context); - this.iResource = getIResource(resource); - } -/** - * A copy constructor for FileConfig objects. Children of this class can - * use this constructor to obtain a copy of a parent object. - * - * @param fileConfig An object of FileConfig - * @throws IOException If the resource tracked by {@code fileConfig} has an invalid URI - */ - protected FileConfig(FileConfig fileConfig) throws IOException { - this.resource = fileConfig.resource; - this.context = fileConfig.context; - - this.srcFile = fileConfig.srcFile; - - this.srcPath = srcFile.getParent(); - this.srcPkgPath = fileConfig.srcPkgPath; - this.srcGenBasePath = fileConfig.srcGenBasePath; - this.name = nameWithoutExtension(this.srcFile); - this.srcGenPath = getSrcGenPath(this.srcGenBasePath, this.srcPkgPath, - this.srcPath, name); - this.srcGenPkgPath = this.srcGenPath; - this.outPath = srcGenBasePath.getParent(); - this.binPath = getBinPath(this.srcPkgPath, this.srcPath, this.outPath, context); - this.iResource = getIResource(resource); - } - - // Getters to be overridden in derived classes. - - protected void setSrcGenPath(Path srcGenPath) { - this.srcGenPath = srcGenPath; - } - - - /** - * Get the iResource corresponding to the provided resource if it can be - * found. - * @throws IOException If the given resource has an invalid URI. - */ - public IResource getIResource(Resource r) throws IOException { - IResource iResource = null; - java.net.URI uri = toPath(r).toFile().toURI(); - if (r.getURI().isPlatform()) { - IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); - IFile[] files = workspaceRoot.findFilesForLocationURI(uri); - if (files != null && files.length > 0 && files[0] != null) { - iResource = files[0]; - } - } else { - // FIXME: find the iResource outside Eclipse - } - return iResource; - } - - /** - * Get the specified path as an Eclipse IResource or, if it is not found, then - * return the iResource for the main file. - * - */ - public IResource getIResource(Path path) { - return getIResource(path.toUri()); - } - - /** - * Get the specified uri as an Eclipse IResource or, if it is not found, then - * return the iResource for the main file. - * For some inexplicable reason, Eclipse uses a mysterious parallel to the file - * system, and when running in EPOCH mode, for some things, you cannot access - * files by referring to their file system location. Instead, you have to refer - * to them relative the workspace root. This is required, for example, when marking - * the file with errors or warnings or when deleting those marks. - * - * @param uri A java.net.uri of the form "file://path". - */ - public IResource getIResource(java.net.URI uri) { - IResource resource = iResource; // Default resource. - // For some peculiar reason known only to Eclipse developers, - // the resource cannot be used directly but has to be converted - // a resource relative to the workspace root. - IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); - - IFile[] files = workspaceRoot.findFilesForLocationURI(uri); - if (files != null && files.length > 0 && files[0] != null) { - resource = files[0]; - } - return resource; - } - - /** - * Get the file name of a resource without file extension - */ - public static String getName(Resource r) throws IOException { - return nameWithoutExtension(toPath(r)); + Path binRoot = outPath.resolve(DEFAULT_BIN_DIR); + this.binPath = useHierarchicalBin ? binRoot.resolve(getSubPkgPath(srcPath)) : binRoot; } /** * Get the directory a resource is located in relative to the root package */ public Path getDirectory(Resource r) throws IOException { - return getSubPkgPath(this.srcPkgPath, toPath(r).getParent()); + return getSubPkgPath(FileUtil.toPath(r).getParent()); } /** @@ -332,30 +190,6 @@ public Path getSrcGenBasePath() { public Path getSrcGenPkgPath() { return srcGenPkgPath; } - - /** - * Return the directory in which to put the generated sources for the - * RTI. By default, this is the same as the regular src-gen directory. - */ - public Path getRTISrcPath() { - return this.srcGenPath; - } - - /** - * Return the directory in which to put the generated binaries for the - * RTI. By default, this is the same as the regular src-gen directory. - */ - public Path getRTIBinPath() { - return this.binPath; - } - - /** - * Return the output directory for generated binary files. - */ - private static Path getBinPath(Path pkgPath, Path srcPath, Path outPath, LFGeneratorContext context) { - Path root = outPath.resolve(DEFAULT_BIN_DIR); - return context.useHierarchicalBin() ? root.resolve(getSubPkgPath(pkgPath, srcPath)) : root; - } /** * Returns the root directory for generated sources. @@ -365,28 +199,21 @@ public static Path getSrcGenRoot(IFileSystemAccess2 fsa) throws IOException { if (srcGenURI.hasTrailingPathSeparator()) { srcGenURI = srcGenURI.trimSegments(1); } - return FileConfig.toPath(srcGenURI); - } - - protected static Path getSrcGenPath(Path srcGenRootPath, Path pkgPath, - Path srcPath, String name) { - return srcGenRootPath.resolve(getSubPkgPath(pkgPath, srcPath)).resolve(name); + return FileUtil.toPath(srcGenURI); } /** - * Given a path that denotes the root of the package and a path - * that denotes the full path to a source file (not including the + * Given a path that denotes the full path to a source file (not including the * file itself), return the relative path from the root of the 'src' * directory, or, if there is no 'src' directory, the relative path - * from the root of the package. - * @param pkgPath The root of the package. + * from the root of the package. * @param srcPath The path to the source. * @return the relative path from the root of the 'src' * directory, or, if there is no 'src' directory, the relative path * from the root of the package */ - protected static Path getSubPkgPath(Path pkgPath, Path srcPath) { - Path relSrcPath = pkgPath.relativize(srcPath); + protected Path getSubPkgPath(Path srcPath) { + Path relSrcPath = srcPkgPath.relativize(srcPath); if (relSrcPath.startsWith(DEFAULT_SRC_DIR)) { int segments = relSrcPath.getNameCount(); if (segments == 1) { @@ -398,308 +225,6 @@ protected static Path getSubPkgPath(Path pkgPath, Path srcPath) { return relSrcPath; } - /** - * Recursively copies the contents of the given 'src' - * directory to 'dest'. Existing files of the destination - * may be overwritten. - * - * @param src The source directory path. - * @param dest The destination directory path. - * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed - * @throws IOException if copy fails. - */ - public static void copyDirectory(final Path src, final Path dest, final boolean skipIfUnchanged) throws IOException { - try (Stream stream = Files.walk(src)) { - stream.forEach(source -> { - // Handling checked exceptions in lambda expressions is - // hard. See - // https://www.baeldung.com/java-lambda-exceptions#handling-checked-exceptions. - // An alternative would be to create a custom Consumer interface and use that - // here. - if (Files.isRegularFile(source)) { // do not copy directories - try { - Path target = dest.resolve(src.relativize(source)); - Files.createDirectories(target.getParent()); - copyFile(source, target, skipIfUnchanged); - } catch (IOException e) { - throw new RuntimeIOException(e); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }); - } - } - - /** - * Recursively copies the contents of the given 'src' - * directory to 'dest'. Existing files of the destination - * may be overwritten. - * - * @param src The source directory path. - * @param dest The destination directory path. - * @throws IOException if copy fails. - */ - public static void copyDirectory(final Path src, final Path dest) throws IOException { - copyDirectory(src, dest, false); - } - - /** - * Copy a given file from 'source' to 'destination'. - * - * This also creates new directories for any directories on the destination - * path that do not yet exist. - * - * @param source The source file path. - * @param destination The destination file path. - * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed - * @throws IOException if copy fails. - */ - public static void copyFile(Path source, Path destination, boolean skipIfUnchanged) throws IOException { - BufferedInputStream stream = new BufferedInputStream(new FileInputStream(source.toFile())); - try (stream) { - copyInputStream(stream, destination, skipIfUnchanged); - } - } - - /** - * Copy a given file from 'source' to 'destination'. - * - * This also creates new directories for any directories on the destination - * path that do not yet exist. - * - * @param source The source file path. - * @param destination The destination file path. - * @throws IOException if copy fails. - */ - public static void copyFile(Path source, Path destination) throws IOException { - copyFile(source, destination, false); - } - - /** - * Copy a given input stream to a destination file. - * - * This also creates new directories for any directories on the destination - * path that do not yet exist. - * - * @param source The source input stream. - * @param destination The destination file path. - * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed - * @throws IOException if copy fails. - */ - private static void copyInputStream(InputStream source, Path destination, boolean skipIfUnchanged) throws IOException { - Files.createDirectories(destination.getParent()); - if(skipIfUnchanged && Files.isRegularFile(destination)) { - if (Arrays.equals(source.readAllBytes(), Files.readAllBytes(destination))) { - return; - } - } - Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING); - } - - - /** - * Lookup a file in the classpath and copy its contents to a destination path - * in the filesystem. - * - * This also creates new directories for any directories on the destination - * path that do not yet exist. - * - * @param source The source file as a path relative to the classpath. - * @param destination The file system path that the source file is copied to. - * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed - * @throws IOException If the given source cannot be copied. - */ - public void copyFileFromClassPath(final String source, final Path destination, final boolean skipIfUnchanged) throws IOException { - InputStream sourceStream = this.getClass().getResourceAsStream(source); - - // Copy the file. - if (sourceStream == null) { - throw new IOException(unableToLocateResourceMessage(source)); - } else { - try (sourceStream) { - copyInputStream(sourceStream, destination, skipIfUnchanged); - } - } - } - - /** - * Lookup a file in the classpath and copy its contents to a destination path - * in the filesystem. - * - * This also creates new directories for any directories on the destination - * path that do not yet exist. - * - * @param source The source file as a path relative to the classpath. - * @param destination The file system path that the source file is copied to. - * @throws IOException If the given source cannot be copied. - */ - public void copyFileFromClassPath(final String source, final Path destination) throws IOException { - copyFileFromClassPath(source, destination, false); - } - - /** - * Lookup a directory in the classpath and copy its contents to a destination path - * in the filesystem. - * - * This also creates new directories for any directories on the destination - * path that do not yet exist. - * - * @param source The source directory as a path relative to the classpath. - * @param destination The file system path that the source directory is copied to. - * @param skipIfUnchanged If true, don't overwrite the file if its content would not be changed - * @throws IOException If the given source cannot be copied. - */ - public void copyDirectoryFromClassPath(final String source, final Path destination, final boolean skipIfUnchanged) throws IOException { - final URL resource = getClass().getResource(source); - if (resource == null) { - throw new IOException(unableToLocateResourceMessage(source)); - } - - final URLConnection connection = resource.openConnection(); - if (connection instanceof JarURLConnection) { - copyDirectoryFromJar((JarURLConnection) connection, destination, skipIfUnchanged); - } else { - try { - Path dir = Paths.get(FileLocator.toFileURL(resource).toURI()); - copyDirectory(dir, destination, skipIfUnchanged); - } catch(URISyntaxException e) { - // This should never happen as toFileURL should always return a valid URL - throw new IOException("Unexpected error while resolving " + source + " on the classpath"); - } - } - } - - /** - * Copy a directory from ta jar to a destination path in the filesystem. - * - * This method should only be used in standalone mode (lfc). - * - * This also creates new directories for any directories on the destination - * path that do not yet exist - * - * @param connection a URLConnection to the source directory within the jar - * @param destination The file system path that the source directory is copied to. - * @param skipIfUnchanged If true, don't overwrite the file if its content would not be changed - * @throws IOException If the given source cannot be copied. - */ - private void copyDirectoryFromJar(JarURLConnection connection, final Path destination, final boolean skipIfUnchanged) throws IOException { - final JarFile jar = connection.getJarFile(); - final String connectionEntryName = connection.getEntryName(); - - // Iterate all entries in the jar file. - for (Enumeration e = jar.entries(); e.hasMoreElements(); ) { - final JarEntry entry = e.nextElement(); - final String entryName = entry.getName(); - - // Extract files only if they match the given source path. - if (entryName.startsWith(connectionEntryName)) { - String filename = entry.getName().substring(connectionEntryName.length() + 1); - Path currentFile = destination.resolve(filename); - - if (entry.isDirectory()) { - Files.createDirectories(currentFile); - } else { - InputStream is = jar.getInputStream(entry); - try (is) { - copyInputStream(is, currentFile, skipIfUnchanged); - } - } - } - } - } - - /** - * Copy a list of files from a given source directory to a given destination directory. - * @param srcDir The directory to copy files from. - * @param dstDir The directory to copy files to. - * @param files The list of files to copy. - * @throws IOException If any of the given files cannot be copied. - */ - public void copyFilesFromClassPath(String srcDir, Path dstDir, List files) throws IOException { - for (String file : files) { - copyFileFromClassPath(srcDir + '/' + file, dstDir.resolve(file)); - } - } - - /** - * Copy the 'fileName' from the 'srcDirectory' to the 'destinationDirectory'. - * This function has a fallback search mechanism, where if `fileName` is not - * found in the `srcDirectory`, it will try to find `fileName` via the following procedure: - * 1- Search in LF_CLASSPATH. @see findFile() - * 2- Search in CLASSPATH. @see findFile() - * 3- Search for 'fileName' as a resource. - * That means the `fileName` can be '/path/to/class/resource'. @see java.lang.Class.getResourceAsStream() - * - * @param fileName Name of the file - * @param srcDir Where the file is currently located - * @param dstDir Where the file should be placed - * @return The name of the file in destinationDirectory - */ - public String copyFileOrResource(String fileName, Path srcDir, Path dstDir) { - // Try to copy the file from the file system. - Path file = findFile(fileName, srcDir); - if (file != null) { - Path target = dstDir.resolve(file.getFileName()); - try { - Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING); - return file.getFileName().toString(); - } catch (IOException e) { - // Files has failed to copy the file, possibly since - // it doesn't exist. Will try to find the file as a - // resource before giving up. - } - } - - // Try to copy the file as a resource. - // If this is missing, it should have been previously reported as an error. - try { - String filenameWithoutPath = fileName; - int lastSeparator = fileName.lastIndexOf(File.separator); - if (lastSeparator > 0) { - filenameWithoutPath = fileName.substring(lastSeparator + 1); // FIXME: brittle. What if the file is in a subdirectory? - } - copyFileFromClassPath(fileName, dstDir.resolve(filenameWithoutPath)); - return filenameWithoutPath; - } catch (IOException ex) { - // Ignore. Previously reported as a warning. - System.err.println("WARNING: Failed to find file " + fileName); - } - - return ""; - } - - /** - * Check if a clean was requested from the standalone compiler and perform - * the clean step. - */ - public void cleanIfNeeded() { - if (context.getArgs().containsKey("clean")) { - try { - doClean(); - } catch (IOException e) { - System.err.println("WARNING: IO Error during clean"); - } - } - } - - /** - * Recursively delete a directory if it exists. - * - * @throws IOException If an I/O error occurs. - */ - public void deleteDirectory(Path dir) throws IOException { - if (Files.isDirectory(dir)) { - System.out.println("Cleaning " + dir); - List pathsToDelete = Files.walk(dir) - .sorted(Comparator.reverseOrder()) - .collect(Collectors.toList()); - for (Path path : pathsToDelete) { - Files.deleteIfExists(path); - } - } - } - /** * Clean any artifacts produced by the code generator and target compilers. * @@ -710,57 +235,14 @@ public void deleteDirectory(Path dir) throws IOException { * @throws IOException If an I/O error occurs. */ public void doClean() throws IOException { - deleteDirectory(binPath); - deleteDirectory(srcGenBasePath); - } - - /** - * Remove files in the bin directory that may have been created. - * Call this if a compilation occurs so that files from a previous - * version do not accidentally get executed. - */ - public void deleteBinFiles() { - String name = nameWithoutExtension(this.srcFile); - String[] files = this.binPath.toFile().list(); - List federateNames = new LinkedList<>(); // FIXME: put this in ASTUtils? - resource.getAllContents().forEachRemaining(node -> { - if (node instanceof Reactor) { - Reactor r = (Reactor) node; - if (r.isFederated()) { - r.getInstantiations().forEach(inst -> federateNames - .add(inst.getName())); - } - } - }); - for (String f : files) { - // Delete executable file or launcher script, if any. - // Delete distribution file, if any. - // Delete RTI file, if any. - if (f.equals(name) || f.equals(getRTIBinName()) - || f.equals(getRTIDistributionScriptName())) { - //noinspection ResultOfMethodCallIgnored - this.binPath.resolve(f).toFile().delete(); - } - // Delete federate executable files, if any. - for (String federateName : federateNames) { - if (f.equals(name + "_" + federateName)) { - //noinspection ResultOfMethodCallIgnored - this.binPath.resolve(f).toFile().delete(); - } - } - } - } - - public static String nameWithoutExtension(Path file) { - String name = file.getFileName().toString(); - int idx = name.lastIndexOf('.'); - return idx < 0 ? name : name.substring(0, idx); + FileUtil.deleteDirectory(binPath); + FileUtil.deleteDirectory(srcGenBasePath); } - + private static Path getPkgPath(Resource resource) throws IOException { if (resource.getURI().isPlatform()) { // We are in the RCA. - File srcFile = toPath(resource).toFile(); + File srcFile = FileUtil.toPath(resource).toFile(); for (IProject r : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { Path p = Paths.get(r.getLocation().toFile().getAbsolutePath()); Path f = Paths.get(srcFile.getAbsolutePath()); @@ -769,7 +251,7 @@ private static Path getPkgPath(Resource resource) throws IOException { } } } - return findPackageRoot(toPath(resource), s -> {}); + return findPackageRoot(FileUtil.toPath(resource), s -> {}); } /** @@ -794,139 +276,5 @@ public static Path findPackageRoot(final Path input, final Consumer prin } while (!p.toFile().getName().equals("src")); return p.getParent(); } - - /** - * Return a java.nio.Path object corresponding to the given URI. - * @throws IOException If the given URI is invalid. - */ - public static Path toPath(URI uri) throws IOException { - return Paths.get(toIPath(uri).toFile().getAbsolutePath()); - } - - /** - * Return a java.nio.Path object corresponding to the given Resource. - * @throws IOException If the given resource has an invalid URI. - */ - public static Path toPath(Resource resource) throws IOException { - return FileConfig.toPath(resource.getURI()); - } - - /** - * Return an org.eclipse.core.runtime.Path object corresponding to the - * given URI. - * @throws IOException If the given URI is invalid. - */ - public static IPath toIPath(URI uri) throws IOException { - if (uri.isPlatform()) { - IPath path = new org.eclipse.core.runtime.Path(uri.toPlatformString(true)); - if (path.segmentCount() == 1) { - return ResourcesPlugin.getWorkspace().getRoot().getProject(path.lastSegment()).getLocation(); - } else { - return ResourcesPlugin.getWorkspace().getRoot().getFile(path).getLocation(); - } - } else if (uri.isFile()) { - return new org.eclipse.core.runtime.Path(uri.toFileString()); - } else { - throw new IOException("Unrecognized file protocol in URI " + uri); - } - } - - /** - * Convert a given path to a unix-style string. - * - * This ensures that '/' is used instead of '\' as file separator. - */ - public static String toUnixString(Path path) { - return path.toString().replace('\\', '/'); - } - - /** - * Search for a given file name in the given directory. - * If not found, search in directories in LF_CLASSPATH. - * If there is no LF_CLASSPATH environment variable, use CLASSPATH, - * if it is defined. - * The first file found will be returned. - * - * @param fileName The file name or relative path + file name - * in plain string format - * @param directory String representation of the director to search in. - * @return A Java file or null if not found - */ - public static Path findFile(String fileName, Path directory) { - Path foundFile; - - // Check in local directory - foundFile = directory.resolve(fileName); - if (Files.isRegularFile(foundFile)) { - return foundFile; - } - - // Check in LF_CLASSPATH - // Load all the resources in LF_CLASSPATH if it is set. - String classpathLF = System.getenv("LF_CLASSPATH"); - if (classpathLF == null) { - classpathLF = System.getenv("CLASSPATH"); - } - if (classpathLF != null) { - String[] paths = classpathLF.split(System.getProperty("path.separator")); - for (String path : paths) { - foundFile = Paths.get(path).resolve(fileName); - if (Files.isRegularFile(foundFile)) { - return foundFile; - } - } - } - // Not found. - return null; - } - /** - * Return the name of the RTI executable. - * @return The name of the RTI executable. - */ - public String getRTIBinName() { - return nameWithoutExtension(srcFile) + RTI_BIN_SUFFIX; - } - - /** - * Return the file location of the RTI executable. - * @return The file location of the RTI executable. - */ - public File getRTIBinFile() { - return this.binPath.resolve(getRTIBinName()).toFile(); - } - - /** - * Return the name of the RTI distribution script. - * @return The name of the RTI distribution script. - */ - public String getRTIDistributionScriptName() { - return nameWithoutExtension(srcFile) + RTI_DISTRIBUTION_SCRIPT_SUFFIX; - } - - /** - * Return the name of the file associated with the given resource, - * excluding its file extension. - * @param r Any {@code Resource}. - * @return The name of the file associated with the given resource, - * excluding its file extension. - * @throws IOException If the resource has an invalid URI. - */ - public static String nameWithoutExtension(Resource r) throws IOException { - return nameWithoutExtension(toPath(r)); - } - - /** - * Return a string that explains why a resource may not have been found and - * what to do about it. - * - * @param fileName The Name of the file that could not be found. - * @return an explanation. - */ - public static String unableToLocateResourceMessage(String fileName) { - return "A required target resource could not be found: " + fileName + "\n" + - "Perhaps a git submodule is not initialized and/or not up to date.\n" + - "To initialize and update, run `git submodule update --init`.\n" + - "You may also need to refresh and clean your build system or IDE."; - } } diff --git a/org.lflang/src/org/lflang/FileConfigExtensions.kt b/org.lflang/src/org/lflang/FileConfigExtensions.kt index d89abb0089..9e2fb4ad14 100644 --- a/org.lflang/src/org/lflang/FileConfigExtensions.kt +++ b/org.lflang/src/org/lflang/FileConfigExtensions.kt @@ -25,20 +25,21 @@ package org.lflang import org.eclipse.emf.ecore.resource.Resource +import org.lflang.util.FileUtil import java.nio.file.Path /** * Get the file name of a resource without file extension */ -val Resource.name: String get() = FileConfig.getName(this) +val Resource.name: String get() = FileUtil.nameWithoutExtension(this) /** Get the path of the receiving resource */ -fun Resource.toPath() = FileConfig.toPath(this) +fun Resource.toPath() = FileUtil.toPath(this) /** * Convert a given path to a unix-style string. * * This ensures that '/' is used instead of '\' as file separator. */ -fun Path.toUnixString(): String = FileConfig.toUnixString(this) +fun Path.toUnixString(): String = FileUtil.toUnixString(this) diff --git a/org.lflang/src/org/lflang/MainConflictChecker.java b/org.lflang/src/org/lflang/MainConflictChecker.java index a15bcbabdc..ce43b9b154 100644 --- a/org.lflang/src/org/lflang/MainConflictChecker.java +++ b/org.lflang/src/org/lflang/MainConflictChecker.java @@ -18,6 +18,7 @@ import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.xtext.xbase.lib.IteratorExtensions; import org.lflang.lf.Reactor; +import org.lflang.util.FileUtil; import com.google.common.collect.Iterables; @@ -98,7 +99,7 @@ public FileVisitResult visitFile(Path path, BasicFileAttributes attr) { // and the name matches, then report the conflict. if (!fileConfig.srcFile.equals(path) && IteratorExtensions.exists(reactors, it -> it.isMain() || it.isFederated()) - && fileConfig.name.equals(FileConfig.nameWithoutExtension(path))) { + && fileConfig.name.equals(FileUtil.nameWithoutExtension(path))) { conflicts.add( fileConfig.srcPath.relativize(path).toString()); } diff --git a/org.lflang/src/org/lflang/TargetConfig.java b/org.lflang/src/org/lflang/TargetConfig.java index b1cba21600..3e667992e0 100644 --- a/org.lflang/src/org/lflang/TargetConfig.java +++ b/org.lflang/src/org/lflang/TargetConfig.java @@ -317,15 +317,6 @@ public static class DockerOptions { public String from = "alpine:latest"; } - public enum Mode { - STANDALONE, - EPOCH, - LSP_FAST, - LSP_MEDIUM, - LSP_SLOW, - UNDEFINED - } - /** * Settings related to tracing options. */ diff --git a/org.lflang/src/org/lflang/TargetProperty.java b/org.lflang/src/org/lflang/TargetProperty.java index 06d5c1e89c..fe0b31f3b4 100644 --- a/org.lflang/src/org/lflang/TargetProperty.java +++ b/org.lflang/src/org/lflang/TargetProperty.java @@ -43,6 +43,7 @@ import org.lflang.lf.Element; import org.lflang.lf.KeyValuePair; import org.lflang.lf.KeyValuePairs; +import org.lflang.util.FileUtil; import org.lflang.validation.LFValidator; /** @@ -209,7 +210,7 @@ public enum TargetProperty { * Directive to stage particular files on the class path to be * processed by the code generator. */ - FILES("files", UnionType.FILE_OR_FILE_ARRAY, Target.ALL, + FILES("files", UnionType.FILE_OR_FILE_ARRAY, List.of(Target.C, Target.CCPP, Target.Python), (config, value, err) -> { config.fileNames = ASTUtils.toListOfStrings(value); }, @@ -411,7 +412,7 @@ public enum TargetProperty { List.of(Target.Rust), (config, value, err) -> { Path referencePath; try { - referencePath = FileConfig.toPath(value.eResource().getURI()).toAbsolutePath(); + referencePath = FileUtil.toPath(value.eResource().getURI()).toAbsolutePath(); } catch (IOException e) { err.reportError(value, "Invalid path? " + e.getMessage()); throw new RuntimeIOException(e); diff --git a/org.lflang/src/org/lflang/federated/FedFileConfig.java b/org.lflang/src/org/lflang/federated/FedFileConfig.java index 130f2eebcd..b6bd0c61b6 100644 --- a/org.lflang/src/org/lflang/federated/FedFileConfig.java +++ b/org.lflang/src/org/lflang/federated/FedFileConfig.java @@ -25,7 +25,6 @@ package org.lflang.federated; -import java.io.File; import java.io.IOException; import org.lflang.FileConfig; @@ -41,17 +40,7 @@ public class FedFileConfig extends FileConfig { /** Name of the federate for this FedFileConfig */ - protected String federateName; - - /** - * Copy constructor for FedFileConfig. - * - * @param fedFileConfig The existing instance of 'FedFileConfig'. - * @throws IOException - */ - public FedFileConfig(FedFileConfig fedFileConfig) throws IOException { - this(fedFileConfig, fedFileConfig.federateName); - } + protected final String federateName; /** * Create an instance of FedFileConfig for federate 'federateName' from an existing @@ -61,13 +50,12 @@ public FedFileConfig(FedFileConfig fedFileConfig) throws IOException { * @param federateName The name of the federate. * @throws IOException */ - public FedFileConfig(FileConfig fileConfig, String federateName) throws IOException { - super(fileConfig); + public FedFileConfig(final FileConfig fileConfig, final String federateName) throws IOException { + super(fileConfig.resource, fileConfig.getSrcGenBasePath(), fileConfig.useHierarchicalBin); this.federateName = federateName; - // The generated code for each federate should be located at super.getSrcGenPath() + "/federateName/" - this.setSrcGenPath(getSrcGenPath(this.srcGenBasePath, this.srcPkgPath, - this.srcPath, name + File.separator + federateName)); + // The generated code for each federate should be located at fileConfig.srcGenPath + "/federateName/" + this.srcGenPath = fileConfig.getSrcGenPath().resolve(federateName); } } diff --git a/org.lflang/src/org/lflang/generator/EclipseErrorReporter.java b/org.lflang/src/org/lflang/generator/EclipseErrorReporter.java index a09295a691..922fda06ce 100644 --- a/org.lflang/src/org/lflang/generator/EclipseErrorReporter.java +++ b/org.lflang/src/org/lflang/generator/EclipseErrorReporter.java @@ -38,11 +38,13 @@ import org.eclipse.xtext.validation.EObjectDiagnosticImpl; import org.lflang.ErrorReporter; import org.lflang.FileConfig; -import org.lflang.TargetConfig.Mode; +import org.lflang.util.FileUtil; /** * An error reporter that prints messages to the command line output and also - * sets markers in the Eclipse IDE if running in integrated mode. + * sets markers in the Epoch IDE. + * + * This class should only be used in EPOCH Mode. */ public class EclipseErrorReporter implements ErrorReporter { @@ -73,7 +75,7 @@ private String report(String message, Severity severity, EObject obj) { final int line = diagnostic.getLine(); Path file = null; try { - file = FileConfig.toPath(diagnostic.getUriToProblem()); + file = FileUtil.toPath(diagnostic.getUriToProblem()); } catch (IOException e) { // just continue with null } @@ -109,43 +111,39 @@ private String report(String message, Severity severity, Integer line, Path file System.err.println(header + ": " + file.toString() + " line " + line.toString() + "\n" + message); - // If running in EPOCH mode, create a marker in the IDE for the - // error. - // See: - // https://help.eclipse.org/2020-03/index.jsp?topic=%2Forg.eclipse.platform.doc.isv%2Fguide%2FresAdv_markers.htm - if (fileConfig.context.getMode() == Mode.EPOCH) { - final IResource iResource = file != null - ? fileConfig.getIResource(file) - : fileConfig.iResource; - - try { - IMarker marker = iResource.createMarker(IMarker.PROBLEM); - - // Mark as LF compilation marker to be able to remove marker at next compile run - marker.setAttribute(this.getClass().getName(), true); - - marker.setAttribute(IMarker.MESSAGE, message); - marker.setAttribute(IMarker.LINE_NUMBER, - line == null ? 1 : line); - // Human-readable line number information. - marker.setAttribute(IMarker.LOCATION, - line == null ? "1" : line.toString()); - // Mark as an error or warning. - marker.setAttribute(IMarker.SEVERITY, isError ? IMarker.SEVERITY_ERROR : IMarker.SEVERITY_WARNING); - marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH); - - marker.setAttribute(IMarker.USER_EDITABLE, false); - - } catch (CoreException e) { - // Ignore, but print a warning - System.err.println("WARNING: Setting markers in the IDE failed:\n" + e.toString()); - } + // Create a marker in the IDE for the error. + // See: https://help.eclipse.org/2020-03/index.jsp?topic=%2Forg.eclipse.platform.doc.isv%2Fguide%2FresAdv_markers.htm + final IResource iResource = file != null + ? FileUtil.getIResource(file) + : FileUtil.getIResource(fileConfig.resource); + + try { + IMarker marker = iResource.createMarker(IMarker.PROBLEM); + + // Mark as LF compilation marker to be able to remove marker at next compile run + marker.setAttribute(this.getClass().getName(), true); - // NOTE: It might be useful to set a start and end. - // marker.setAttribute(IMarker.CHAR_START, 0); - // marker.setAttribute(IMarker.CHAR_END, 5); + marker.setAttribute(IMarker.MESSAGE, message); + marker.setAttribute(IMarker.LINE_NUMBER, + line == null ? 1 : line); + // Human-readable line number information. + marker.setAttribute(IMarker.LOCATION, + line == null ? "1" : line.toString()); + // Mark as an error or warning. + marker.setAttribute(IMarker.SEVERITY, isError ? IMarker.SEVERITY_ERROR : IMarker.SEVERITY_WARNING); + marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH); + + marker.setAttribute(IMarker.USER_EDITABLE, false); + + } catch (CoreException e) { + // Ignore, but print a warning + System.err.println("WARNING: Setting markers in the IDE failed:\n" + e.toString()); } + // NOTE: It might be useful to set a start and end. + // marker.setAttribute(IMarker.CHAR_START, 0); + // marker.setAttribute(IMarker.CHAR_END, 5); + // Return a string that can be inserted into the generated code. return header + ": " + message; } @@ -235,7 +233,7 @@ public boolean getErrorsOccurred() { */ public void clearMarkers() { try { - IResource resource = fileConfig.getIResource(fileConfig.srcFile); + IResource resource = FileUtil.getIResource(fileConfig.srcFile); IMarker[] markers = resource.findMarkers(null, true, IResource.DEPTH_INFINITE); for (IMarker marker : markers) { diff --git a/org.lflang/src/org/lflang/generator/GeneratorBase.xtend b/org.lflang/src/org/lflang/generator/GeneratorBase.xtend index 1c5631b2a0..30357fabbf 100644 --- a/org.lflang/src/org/lflang/generator/GeneratorBase.xtend +++ b/org.lflang/src/org/lflang/generator/GeneratorBase.xtend @@ -25,7 +25,7 @@ package org.lflang.generator import java.io.File -import java.nio.file.Files +import java.io.IOException; import java.nio.file.Paths import java.util.ArrayList import java.util.Collection @@ -47,7 +47,6 @@ import org.lflang.InferredType import org.lflang.MainConflictChecker import org.lflang.Target import org.lflang.TargetConfig -import org.lflang.TargetConfig.Mode import org.lflang.TargetProperty.CoordinationType import org.lflang.TimeUnit import org.lflang.TimeValue @@ -292,13 +291,13 @@ abstract class GeneratorBase extends AbstractLFValidator { */ def void doGenerate(Resource resource, LFGeneratorContext context) { - JavaGeneratorUtils.setTargetConfig( - context, JavaGeneratorUtils.findTarget(fileConfig.resource), targetConfig, errorReporter + GeneratorUtils.setTargetConfig( + context, GeneratorUtils.findTarget(fileConfig.resource), targetConfig, errorReporter ) - fileConfig.cleanIfNeeded() + cleanIfNeeded(context) - printInfo() + printInfo(context.mode) // Clear any IDE markers that may have been created by a previous build. // Markers mark problems in the Eclipse IDE when running in integrated mode. @@ -311,7 +310,7 @@ abstract class GeneratorBase extends AbstractLFValidator { createMainInstantiation() // Check if there are any conflicting main reactors elsewhere in the package. - if (context.mode == Mode.STANDALONE && mainDef !== null) { + if (context.mode == LFGeneratorContext.Mode.STANDALONE && mainDef !== null) { for (String conflict : new MainConflictChecker(fileConfig).conflicts) { errorReporter.reportError(this.mainDef.reactorClass, "Conflicting main reactor in " + conflict); } @@ -319,32 +318,31 @@ abstract class GeneratorBase extends AbstractLFValidator { // Configure the command factory commandFactory.setVerbose(); - if (context.mode == Mode.STANDALONE && context.getArgs().containsKey("quiet")) { + if (context.mode == LFGeneratorContext.Mode.STANDALONE && context.getArgs().containsKey("quiet")) { commandFactory.setQuiet(); } // This must be done before desugaring delays below. analyzeFederates(context) - + // Process target files. Copy each of them into the src-gen dir. - // FIXME: Should we do this here? I think the Cpp target doesn't support - // the files property and this doesn't make sense for federates the way it is + // FIXME: Should we do this here? This doesn't make sense for federates the way it is // done here. copyUserFiles(this.targetConfig, this.fileConfig); // Collect reactors and create an instantiation graph. // These are needed to figure out which resources we need // to validate, which happens in setResources(). - setReactorsAndInstantiationGraph() + setReactorsAndInstantiationGraph(context.mode) - JavaGeneratorUtils.validate(context, fileConfig, instantiationGraph, errorReporter) - val allResources = JavaGeneratorUtils.getResources(reactors) + GeneratorUtils.validate(context, fileConfig, instantiationGraph, errorReporter) + val allResources = GeneratorUtils.getResources(reactors) resources.addAll(allResources.stream() // FIXME: This filter reproduces the behavior of the method it replaces. But why must it be so complicated? Why are we worried about weird corner cases like this? .filter [it | it != fileConfig.resource || (mainDef !== null && it === mainDef.reactorClass.eResource)] - .map [it | JavaGeneratorUtils.getLFResource(it, fileConfig.getSrcGenBasePath(), context, errorReporter)] + .map [it | GeneratorUtils.getLFResource(it, fileConfig.getSrcGenBasePath(), context, errorReporter)] .collect(Collectors.toList()) ) - JavaGeneratorUtils.accommodatePhysicalActionsIfPresent(allResources, target, targetConfig, errorReporter); + GeneratorUtils.accommodatePhysicalActionsIfPresent(allResources, target, targetConfig, errorReporter); // FIXME: Should the GeneratorBase pull in `files` from imported // resources? @@ -358,8 +356,8 @@ abstract class GeneratorBase extends AbstractLFValidator { // Invoke these functions a second time because transformations // may have introduced new reactors! - setReactorsAndInstantiationGraph() - + setReactorsAndInstantiationGraph(context.mode) + // Check for existence and support of modes hasModalReactors = reactors.exists[!modes.empty] checkModalReactorSupport(false) @@ -367,6 +365,20 @@ abstract class GeneratorBase extends AbstractLFValidator { enableSupportForSerializationIfApplicable(context.cancelIndicator); } + /** + * Check if a clean was requested from the standalone compiler and perform + * the clean step. + */ + protected def cleanIfNeeded(LFGeneratorContext context) { + if (context.getArgs().containsKey("clean")) { + try { + fileConfig.doClean(); + } catch (IOException e) { + System.err.println("WARNING: IO Error during clean"); + } + } + } + /** * Create a new instantiation graph. This is a graph where each node is a Reactor (not a ReactorInstance) * and an arc from Reactor A to Reactor B means that B contains an instance of A, constructed with a statement @@ -375,7 +387,7 @@ abstract class GeneratorBase extends AbstractLFValidator { * Hence, after this method returns, `this.reactors` will be a list of Reactors such that any * reactor is preceded in the list by reactors that it instantiates. */ - protected def setReactorsAndInstantiationGraph() { + protected def setReactorsAndInstantiationGraph(LFGeneratorContext.Mode mode) { // Build the instantiation graph . this.instantiationGraph = new InstantiationGraph(fileConfig.resource, false) @@ -387,7 +399,7 @@ abstract class GeneratorBase extends AbstractLFValidator { // If there is no main reactor or if all reactors in the file need to be validated, then make sure the reactors // list includes even reactors that are not instantiated anywhere. - if (mainDef === null || fileConfig.context.mode == Mode.LSP_MEDIUM) { + if (mainDef === null || mode == LFGeneratorContext.Mode.LSP_MEDIUM) { for (r : fileConfig.resource.allContents.toIterable.filter(Reactor)) { if (!this.reactors.contains(r)) { this.reactors.add(r); @@ -406,34 +418,14 @@ abstract class GeneratorBase extends AbstractLFValidator { } /** - * Copy all files listed in the target property `files` into the - * src-gen folder of the main .lf file. + * Copy user specific files to the src-gen folder. + * + * This should be overridden by the target generators. * * @param targetConfig The targetConfig to read the `files` from. * @param fileConfig The fileConfig used to make the copy and resolve paths. */ - protected def copyUserFiles(TargetConfig targetConfig, FileConfig fileConfig) { - // Make sure the target directory exists. - val targetDir = this.fileConfig.getSrcGenPath - Files.createDirectories(targetDir) - - for (filename : targetConfig.fileNames) { - val relativeFileName = fileConfig.copyFileOrResource( - filename, - fileConfig.srcFile.parent, - targetDir); - if (relativeFileName.isNullOrEmpty) { - errorReporter.reportError( - "Failed to find file " + filename + " specified in the" + - " files target property." - ) - } else { - this.targetConfig.filesNamesWithoutPath.add( - relativeFileName - ); - } - } - } + protected def void copyUserFiles(TargetConfig targetConfig, FileConfig fileConfig) {} /** * Return true if errors occurred in the last call to doGenerate(). @@ -567,54 +559,6 @@ abstract class GeneratorBase extends AbstractLFValidator { return unit.canonicalName.toUpperCase } - /** - * Run the custom build command specified with the "build" parameter. - * This command is executed in the same directory as the source file. - * - * The following environment variables will be available to the command: - * - * * LF_CURRENT_WORKING_DIRECTORY: The directory in which the command is invoked. - * * LF_SOURCE_DIRECTORY: The directory containing the .lf file being compiled. - * * LF_SOURCE_GEN_DIRECTORY: The directory in which generated files are placed. - * * LF_BIN_DIRECTORY: The directory into which to put binaries. - * - */ - protected def runBuildCommand() { - var commands = new ArrayList - for (cmd : targetConfig.buildCommands) { - val tokens = newArrayList(cmd.split("\\s+")) - if (tokens.size > 0) { - val buildCommand = commandFactory.createCommand( - tokens.head, - tokens.tail.toList, - this.fileConfig.srcPath - ) - // If the build command could not be found, abort. - // An error has already been reported in createCommand. - if (buildCommand === null) { - return - } - commands.add(buildCommand) - } - } - - for (cmd : commands) { - // execute the command - val returnCode = cmd.run() - - if (returnCode != 0 && fileConfig.context.mode === Mode.STANDALONE) { - errorReporter.reportError('''Build command "«targetConfig.buildCommands»" returns error code «returnCode»''') - return - } - // For warnings (vs. errors), the return code is 0. - // But we still want to mark the IDE. - if (cmd.errors.toString.length > 0 && fileConfig.context.mode !== Mode.STANDALONE) { - reportCommandErrors(cmd.errors.toString()) - return - } - } - } - // ////////////////////////////////////////// // // Protected methods. @@ -1271,10 +1215,9 @@ abstract class GeneratorBase extends AbstractLFValidator { * Print to stdout information about what source file is being generated, * what mode the generator is in, and where the generated sources are to be put. */ - def printInfo() { + def printInfo(LFGeneratorContext.Mode mode) { println("Generating code for: " + fileConfig.resource.getURI.toString) - println('******** mode: ' + fileConfig.context.mode) - println('******** source file: ' + fileConfig.srcFile) // FIXME: redundant + println('******** mode: ' + mode) println('******** generated sources: ' + fileConfig.getSrcGenPath) } diff --git a/org.lflang/src/org/lflang/generator/JavaGeneratorUtils.java b/org.lflang/src/org/lflang/generator/GeneratorUtils.java similarity index 90% rename from org.lflang/src/org/lflang/generator/JavaGeneratorUtils.java rename to org.lflang/src/org/lflang/generator/GeneratorUtils.java index 87bb2393eb..70caa9708b 100644 --- a/org.lflang/src/org/lflang/generator/JavaGeneratorUtils.java +++ b/org.lflang/src/org/lflang/generator/GeneratorUtils.java @@ -1,11 +1,9 @@ package org.lflang.generator; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -25,7 +23,7 @@ import org.lflang.FileConfig; import org.lflang.Target; import org.lflang.TargetConfig; -import org.lflang.TargetConfig.Mode; +import org.lflang.generator.LFGeneratorContext.Mode; import org.lflang.TargetProperty; import org.lflang.TargetProperty.SchedulerOption; import org.lflang.graph.InstantiationGraph; @@ -37,6 +35,7 @@ import org.lflang.lf.Model; import org.lflang.lf.Reactor; import org.lflang.lf.TargetDecl; +import org.lflang.util.FileUtil; /** * A helper class with functions that may be useful for code @@ -46,9 +45,9 @@ * instead be in GeneratorUtils.kt, but Eclipse cannot * handle Kotlin files. */ -public class JavaGeneratorUtils { +public class GeneratorUtils { - private JavaGeneratorUtils() { + private GeneratorUtils() { // utility class } @@ -236,7 +235,7 @@ public static void validate( // Report the error on this resource. Path path = null; try { - path = FileConfig.toPath(resource); + path = FileUtil.toPath(resource); } catch (IOException e) { path = Paths.get("Unknown file"); // Not sure if this is what we want. } @@ -300,7 +299,7 @@ public static LFResource getLFResource( LFGeneratorContext context, ErrorReporter errorReporter ) { - TargetDecl target = JavaGeneratorUtils.findTarget(resource); + TargetDecl target = GeneratorUtils.findTarget(resource); KeyValuePairs config = target.getConfig(); var targetConfig = new TargetConfig(); if (config != null) { @@ -309,7 +308,7 @@ public static LFResource getLFResource( } FileConfig fc; try { - fc = new FileConfig(resource, srcGenBasePath, context); + fc = new FileConfig(resource, srcGenBasePath, context.useHierarchicalBin()); } catch (IOException e) { throw new RuntimeException("Failed to instantiate an imported resource because an I/O error " + "occurred."); @@ -318,41 +317,6 @@ public static LFResource getLFResource( } /** - * Write text to a file. - * @param text The text to be written. - * @param path The file to write the code to. - * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed - */ - public static void writeToFile(String text, Path path, boolean skipIfUnchanged) throws IOException { - Files.createDirectories(path.getParent()); - final byte[] bytes = text.getBytes(); - if (skipIfUnchanged && Files.isRegularFile(path)) { - if (Arrays.equals(bytes, Files.readAllBytes(path))) { - return; - } - } - Files.write(path, text.getBytes()); - } - - /** - * Write text to a file. - * @param text The text to be written. - * @param path The file to write the code to. - */ - public static void writeToFile(String text, Path path) throws IOException { - writeToFile(text, path, false); - } - - /** - * Write text to a file. - * @param text The text to be written. - * @param path The file to write the code to. - */ - public static void writeToFile(CharSequence text, Path path) throws IOException { - writeToFile(text.toString(), path, false); - } - - /** * If the mode is Mode.EPOCH (the code generator is running in an * Eclipse IDE), then refresh the project. This will ensure that * any generated files become visible in the project. @@ -360,7 +324,7 @@ public static void writeToFile(CharSequence text, Path path) throws IOException * @param compilerMode An indicator of whether Epoch is running. */ public static void refreshProject(Resource resource, Mode compilerMode) { - if (compilerMode == Mode.EPOCH) { + if (compilerMode == LFGeneratorContext.Mode.EPOCH) { URI uri = resource.getURI(); if (uri.isPlatformResource()) { // This condition should normally be met when running Epoch IResource member = ResourcesPlugin.getWorkspace().getRoot().findMember(uri.toPlatformString(true)); diff --git a/org.lflang/src/org/lflang/generator/IntegratedBuilder.java b/org.lflang/src/org/lflang/generator/IntegratedBuilder.java index 0bab22fff1..f1172cbab9 100644 --- a/org.lflang/src/org/lflang/generator/IntegratedBuilder.java +++ b/org.lflang/src/org/lflang/generator/IntegratedBuilder.java @@ -18,7 +18,7 @@ import org.lflang.ErrorReporter; import org.lflang.FileConfig; -import org.lflang.TargetConfig.Mode; +import org.lflang.generator.LFGeneratorContext.Mode; import com.google.inject.Inject; import com.google.inject.Provider; @@ -125,7 +125,7 @@ private GeneratorResult doGenerate( CancelIndicator cancelIndicator ) { LFGeneratorContext context = new MainContext( - mustComplete ? Mode.LSP_SLOW : Mode.LSP_MEDIUM, cancelIndicator, reportProgress, new Properties(), + mustComplete ? Mode.LSP_SLOW : LFGeneratorContext.Mode.LSP_MEDIUM, cancelIndicator, reportProgress, new Properties(), false, fileConfig -> new LanguageServerErrorReporter(fileConfig.resource.getContents().get(0)) ); generator.generate(getResource(uri), fileAccess, context); diff --git a/org.lflang/src/org/lflang/generator/LFGenerator.java b/org.lflang/src/org/lflang/generator/LFGenerator.java index 688c2814b4..ac0c462c84 100644 --- a/org.lflang/src/org/lflang/generator/LFGenerator.java +++ b/org.lflang/src/org/lflang/generator/LFGenerator.java @@ -6,7 +6,6 @@ import java.nio.file.Path; import java.util.Objects; -import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.xtext.generator.AbstractGenerator; import org.eclipse.xtext.generator.IFileSystemAccess2; @@ -17,7 +16,6 @@ import org.lflang.ErrorReporter; import org.lflang.FileConfig; import org.lflang.Target; -import org.lflang.TargetConfig.Mode; import org.lflang.generator.c.CGenerator; import org.lflang.generator.python.PythonGenerator; import org.lflang.scoping.LFGlobalScopeProvider; @@ -69,15 +67,15 @@ private FileConfig createFileConfig(final Target target, String className = "org.lflang.generator." + target.packageName + "." + target.classNamePrefix + "FileConfig"; try { return (FileConfig) Class.forName(className) - .getDeclaredConstructor(Resource.class, Path.class, LFGeneratorContext.class) - .newInstance(resource, srcGenBasePath, context); + .getDeclaredConstructor(Resource.class, Path.class, boolean.class) + .newInstance(resource, srcGenBasePath, context.useHierarchicalBin()); } catch (InvocationTargetException e) { throw new RuntimeException("Exception instantiating " + className, e.getTargetException()); } catch (ReflectiveOperationException e) { - return new FileConfig(resource, srcGenBasePath, context); + return new FileConfig(resource, srcGenBasePath, context.useHierarchicalBin()); } default: - return new FileConfig(resource, srcGenBasePath, context); + return new FileConfig(resource, srcGenBasePath, context.useHierarchicalBin()); } } @@ -149,7 +147,7 @@ private GeneratorBase createKotlinBaseGenerator(Target target, FileConfig fileCo public void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) { final LFGeneratorContext lfContext = LFGeneratorContext.lfGeneratorContextOf(context, resource); - if (lfContext.getMode() == Mode.LSP_FAST) return; // The fastest way to generate code is to not generate any code. + if (lfContext.getMode() == LFGeneratorContext.Mode.LSP_FAST) return; // The fastest way to generate code is to not generate any code. final Target target = Target.fromDecl(ASTUtils.targetDecl(resource)); assert target != null; diff --git a/org.lflang/src/org/lflang/generator/LFGeneratorContext.java b/org.lflang/src/org/lflang/generator/LFGeneratorContext.java index b4320b3751..e5d39d5fb8 100644 --- a/org.lflang/src/org/lflang/generator/LFGeneratorContext.java +++ b/org.lflang/src/org/lflang/generator/LFGeneratorContext.java @@ -10,7 +10,6 @@ import org.lflang.ErrorReporter; import org.lflang.FileConfig; -import org.lflang.TargetConfig.Mode; import org.lflang.util.LFCommand; /** @@ -22,6 +21,15 @@ */ public interface LFGeneratorContext extends IGeneratorContext { + enum Mode { + STANDALONE, + EPOCH, + LSP_FAST, + LSP_MEDIUM, + LSP_SLOW, + UNDEFINED + } + /** * Return the mode of operation, which indicates how the compiler has been invoked * (e.g., from within Epoch, from the command line, or via a Language Server). @@ -89,7 +97,7 @@ default void finish( Map codeMaps, String interpreter ) { - final boolean isWindows = JavaGeneratorUtils.isHostWindows(); + final boolean isWindows = GeneratorUtils.isHostWindows(); if (execName != null && binPath != null) { Path executable = binPath.resolve(execName + (isWindows && interpreter == null ? ".exe" : "")); String relativeExecutable = fileConfig.srcPkgPath.relativize(executable).toString(); diff --git a/org.lflang/src/org/lflang/generator/MainContext.java b/org.lflang/src/org/lflang/generator/MainContext.java index c786cf2200..ff515f78fd 100644 --- a/org.lflang/src/org/lflang/generator/MainContext.java +++ b/org.lflang/src/org/lflang/generator/MainContext.java @@ -8,7 +8,6 @@ import org.lflang.DefaultErrorReporter; import org.lflang.ErrorReporter; import org.lflang.FileConfig; -import org.lflang.TargetConfig.Mode; import org.lflang.generator.IntegratedBuilder.ReportProgress; /** diff --git a/org.lflang/src/org/lflang/generator/SubContext.java b/org.lflang/src/org/lflang/generator/SubContext.java index 89e0ca7784..fcffaf8806 100644 --- a/org.lflang/src/org/lflang/generator/SubContext.java +++ b/org.lflang/src/org/lflang/generator/SubContext.java @@ -6,7 +6,6 @@ import org.lflang.ErrorReporter; import org.lflang.FileConfig; -import org.lflang.TargetConfig.Mode; /** * A {@code SubContext} is the context of a process within a build process. For example, diff --git a/org.lflang/src/org/lflang/generator/Validator.java b/org.lflang/src/org/lflang/generator/Validator.java index f62f94083c..0ad83f9c35 100644 --- a/org.lflang/src/org/lflang/generator/Validator.java +++ b/org.lflang/src/org/lflang/generator/Validator.java @@ -3,7 +3,6 @@ import org.eclipse.xtext.util.CancelIndicator; import org.lflang.ErrorReporter; -import org.lflang.TargetConfig.Mode; import org.lflang.util.LFCommand; import java.nio.file.Path; import java.util.ArrayList; @@ -88,7 +87,7 @@ private boolean validationEnabled(LFGeneratorContext context) { * @return Whether validation of generated code is enabled by default. */ protected boolean validationEnabledByDefault(LFGeneratorContext context) { - return context.getMode() != Mode.STANDALONE; + return context.getMode() != LFGeneratorContext.Mode.STANDALONE; } /** diff --git a/org.lflang/src/org/lflang/generator/c/CCmakeCompiler.java b/org.lflang/src/org/lflang/generator/c/CCmakeCompiler.java index 4c31b8da77..3621a6eac1 100644 --- a/org.lflang/src/org/lflang/generator/c/CCmakeCompiler.java +++ b/org.lflang/src/org/lflang/generator/c/CCmakeCompiler.java @@ -30,13 +30,14 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; + import org.lflang.ErrorReporter; import org.lflang.FileConfig; -import org.lflang.TargetConfig.Mode; import org.lflang.TargetConfig; import org.lflang.generator.GeneratorBase; -import org.lflang.generator.JavaGeneratorUtils; +import org.lflang.generator.GeneratorUtils; import org.lflang.generator.LFGeneratorContext; +import org.lflang.util.FileUtil; import org.lflang.util.LFCommand; @@ -103,7 +104,7 @@ public boolean runCCompiler( // has previously occurred. Deleting the build directory // if no prior errors have occurred can prolong the compilation // substantially. - fileConfig.deleteDirectory(buildPath); + FileUtil.deleteDirectory(buildPath); // Make sure the build directory exists Files.createDirectories(buildPath); @@ -126,7 +127,7 @@ public boolean runCCompiler( int cMakeReturnCode = compile.run(context.getCancelIndicator()); if (cMakeReturnCode != 0 && - context.getMode() == Mode.STANDALONE && + context.getMode() == LFGeneratorContext.Mode.STANDALONE && !outputContainsKnownCMakeErrors(compile.getErrors().toString())) { errorReporter.reportError(targetConfig.compiler + " failed with error code " + cMakeReturnCode); } @@ -134,7 +135,7 @@ public boolean runCCompiler( // For warnings (vs. errors), the return code is 0. // But we still want to mark the IDE. if (compile.getErrors().toString().length() > 0 && - context.getMode() != Mode.STANDALONE && + context.getMode() != LFGeneratorContext.Mode.STANDALONE && !outputContainsKnownCMakeErrors(compile.getErrors().toString())) { generator.reportCommandErrors(compile.getErrors().toString()); } @@ -147,7 +148,7 @@ public boolean runCCompiler( makeReturnCode = build.run(context.getCancelIndicator()); if (makeReturnCode != 0 && - context.getMode() == Mode.STANDALONE && + context.getMode() == LFGeneratorContext.Mode.STANDALONE && !outputContainsKnownCMakeErrors(build.getErrors().toString())) { errorReporter.reportError(targetConfig.compiler + " failed with error code " + makeReturnCode); } @@ -155,7 +156,7 @@ public boolean runCCompiler( // For warnings (vs. errors), the return code is 0. // But we still want to mark the IDE. if (build.getErrors().toString().length() > 0 && - context.getMode() != Mode.STANDALONE && + context.getMode() != LFGeneratorContext.Mode.STANDALONE && !outputContainsKnownCMakeErrors(build.getErrors().toString())) { generator.reportCommandErrors(build.getErrors().toString()); } @@ -187,16 +188,16 @@ public LFCommand compileCmakeCommand( Path buildPath = fileConfig.getSrcGenPath().resolve("build"); List arguments = new ArrayList(); - arguments.addAll(List.of("-DCMAKE_INSTALL_PREFIX="+FileConfig.toUnixString(fileConfig.getOutPath()), - "-DCMAKE_INSTALL_BINDIR="+FileConfig.toUnixString( + arguments.addAll(List.of("-DCMAKE_INSTALL_PREFIX="+ FileUtil.toUnixString(fileConfig.getOutPath()), + "-DCMAKE_INSTALL_BINDIR="+ FileUtil.toUnixString( fileConfig.getOutPath().relativize( fileConfig.binPath ) ), - FileConfig.toUnixString(fileConfig.getSrcGenPath()) + FileUtil.toUnixString(fileConfig.getSrcGenPath()) )); - if (JavaGeneratorUtils.isHostWindows()) { + if (GeneratorUtils.isHostWindows()) { arguments.add("-DCMAKE_SYSTEM_VERSION=\"10.0\""); } diff --git a/org.lflang/src/org/lflang/generator/c/CCmakeGenerator.java b/org.lflang/src/org/lflang/generator/c/CCmakeGenerator.java index a9effdf5d1..4faabfa2af 100644 --- a/org.lflang/src/org/lflang/generator/c/CCmakeGenerator.java +++ b/org.lflang/src/org/lflang/generator/c/CCmakeGenerator.java @@ -33,6 +33,7 @@ import org.lflang.FileConfig; import org.lflang.TargetConfig; import org.lflang.generator.CodeBuilder; +import org.lflang.util.FileUtil; /** * A helper class that generates a CMakefile that can be used to compile the generated C code. @@ -82,7 +83,7 @@ CodeBuilder generateCMakeCode( for (String file: targetConfig.compileAdditionalSources) { var relativePath = fileConfig.getSrcGenPath().relativize( fileConfig.getSrcGenPath().resolve(Paths.get(file))); - additionalSources.add(FileConfig.toUnixString(relativePath)); + additionalSources.add(FileUtil.toUnixString(relativePath)); } cMakeCode.newLine(); diff --git a/org.lflang/src/org/lflang/generator/c/CCompiler.java b/org.lflang/src/org/lflang/generator/c/CCompiler.java index 7d8ee9ec09..c3296ea6b4 100644 --- a/org.lflang/src/org/lflang/generator/c/CCompiler.java +++ b/org.lflang/src/org/lflang/generator/c/CCompiler.java @@ -29,13 +29,14 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; + import org.lflang.ErrorReporter; import org.lflang.FileConfig; -import org.lflang.TargetConfig.Mode; import org.lflang.TargetConfig; import org.lflang.generator.GeneratorBase; import org.lflang.generator.GeneratorCommandFactory; import org.lflang.generator.LFGeneratorContext; +import org.lflang.util.FileUtil; import org.lflang.util.LFCommand; /** @@ -119,7 +120,7 @@ public boolean runCCompiler( GeneratorBase generator, LFGeneratorContext context ) throws IOException { - if (noBinary && context.getMode() == Mode.STANDALONE) { + if (noBinary && context.getMode() == LFGeneratorContext.Mode.STANDALONE) { errorReporter.reportError("Did not output executable; no main reactor found."); } LFCommand compile = compileCCommand(file, noBinary); @@ -129,12 +130,12 @@ public boolean runCCompiler( int returnCode = compile.run(context.getCancelIndicator()); - if (returnCode != 0 && context.getMode() == Mode.STANDALONE) { + if (returnCode != 0 && context.getMode() == LFGeneratorContext.Mode.STANDALONE) { errorReporter.reportError(targetConfig.compiler+" returns error code "+returnCode); } // For warnings (vs. errors), the return code is 0. // But we still want to mark the IDE. - if (compile.getErrors().toString().length() > 0 && context.getMode() != Mode.STANDALONE) { + if (compile.getErrors().toString().length() > 0 && context.getMode() != LFGeneratorContext.Mode.STANDALONE) { generator.reportCommandErrors(compile.getErrors().toString()); } @@ -167,8 +168,8 @@ public LFCommand compileCCommand( fileConfig.binPath.resolve(Paths.get(fileToCompile))); // NOTE: we assume that any C compiler takes Unix paths as arguments. - String relSrcPathString = FileConfig.toUnixString(relativeSrcPath); - String relBinPathString = FileConfig.toUnixString(relativeBinPath); + String relSrcPathString = FileUtil.toUnixString(relativeSrcPath); + String relBinPathString = FileUtil.toUnixString(relativeBinPath); // If there is no main reactor, then generate a .o file not an executable. if (noBinary) { @@ -180,7 +181,7 @@ public LFCommand compileCCommand( for (String file: targetConfig.compileAdditionalSources) { var relativePath = fileConfig.getOutPath().relativize( fileConfig.getSrcGenPath().resolve(Paths.get(file))); - compileArgs.add(FileConfig.toUnixString(relativePath)); + compileArgs.add(FileUtil.toUnixString(relativePath)); } compileArgs.addAll(targetConfig.compileLibraries); diff --git a/org.lflang/src/org/lflang/generator/c/CGenerator.xtend b/org.lflang/src/org/lflang/generator/c/CGenerator.xtend index e739a4cb07..be989cecb7 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.Files import java.util.ArrayList import java.util.Collection import java.util.HashSet @@ -61,7 +62,7 @@ import org.lflang.generator.CodeBuilder import org.lflang.generator.GeneratorBase import org.lflang.generator.GeneratorResult import org.lflang.generator.IntegratedBuilder -import org.lflang.generator.JavaGeneratorUtils +import org.lflang.generator.GeneratorUtils import org.lflang.generator.LFGeneratorContext import org.lflang.generator.ModeInstance import org.lflang.generator.PortInstance @@ -89,6 +90,7 @@ import org.lflang.lf.StateVar import org.lflang.lf.TriggerRef import org.lflang.lf.VarRef import org.lflang.lf.Variable +import org.lflang.util.FileUtil import org.lflang.util.XtendUtil import static extension org.lflang.ASTUtils.* @@ -333,8 +335,8 @@ class CGenerator extends GeneratorBase { //////////////////////////////////////////// //// Public methods - override printInfo() { - super.printInfo() + override printInfo(LFGeneratorContext.Mode mode) { + super.printInfo(mode) println('******** generated binaries: ' + fileConfig.binPath) } @@ -371,7 +373,7 @@ class CGenerator extends GeneratorBase { def accommodatePhysicalActionsIfPresent() { // If there are any physical actions, ensure the threaded engine is used and that // keepalive is set to true, unless the user has explicitly set it to false. - for (resource : JavaGeneratorUtils.getResources(reactors)) { + for (resource : GeneratorUtils.getResources(reactors)) { for (action : resource.allContents.toIterable.filter(Action)) { if (action.origin == ActionOrigin.PHYSICAL) { // If the unthreaded runtime is requested, use the threaded runtime instead @@ -394,7 +396,7 @@ class CGenerator extends GeneratorBase { * otherwise report an error and return false. */ protected def boolean isOSCompatible() { - if (JavaGeneratorUtils.isHostWindows) { + if (GeneratorUtils.isHostWindows) { if (isFederated) { errorReporter.reportError( "Federated LF programs with a C target are currently not supported on Windows. " + @@ -554,7 +556,7 @@ class CGenerator extends GeneratorBase { targetConfig.filesNamesWithoutPath.clear(); // Re-apply the cmake-include target property of the main .lf file. - val target = JavaGeneratorUtils.findTarget(mainDef.reactorClass.eResource) + val target = GeneratorUtils.findTarget(mainDef.reactorClass.eResource) if (target.config !== null) { // Update the cmake-include TargetProperty.updateOne( @@ -589,7 +591,7 @@ class CGenerator extends GeneratorBase { } // Copy the core lib - fileConfig.copyFilesFromClassPath("/lib/c/reactor-c/core", fileConfig.getSrcGenPath.resolve("core"), coreFiles) + FileUtil.copyFilesFromClassPath("/lib/c/reactor-c/core", fileConfig.getSrcGenPath.resolve("core"), coreFiles) // Copy the header files copyTargetHeaderFile() @@ -854,7 +856,7 @@ class CGenerator extends GeneratorBase { && targetConfig.buildCommands.nullOrEmpty && !federate.isRemote // This code is unreachable in LSP_FAST mode, so that check is omitted. - && context.getMode() != TargetConfig.Mode.LSP_MEDIUM + && 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 @@ -883,7 +885,7 @@ class CGenerator extends GeneratorBase { } if (!cCompiler.runCCompiler(execName, main === null, generator, context)) { // If compilation failed, remove any bin files that may have been created. - threadFileConfig.deleteBinFiles() + 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. if (!isFederated) context.unsuccessfulFinish(); @@ -939,7 +941,7 @@ class CGenerator extends GeneratorBase { } // In case we are in Eclipse, make sure the generated code is visible. - JavaGeneratorUtils.refreshProject(resource, context.mode) + GeneratorUtils.refreshProject(resource, context.mode) } override checkModalReactorSupport(boolean _) { @@ -1171,12 +1173,31 @@ class CGenerator extends GeneratorBase { * @param fileConfig The fileConfig used to make the copy and resolve paths. */ override copyUserFiles(TargetConfig targetConfig, FileConfig fileConfig) { - super.copyUserFiles(targetConfig, fileConfig); - + super.copyUserFiles(targetConfig, fileConfig) + // Make sure the target directory exists. val targetDir = this.fileConfig.getSrcGenPath + Files.createDirectories(targetDir) + + for (filename : targetConfig.fileNames) { + val relativeFileName = CUtil.copyFileOrResource( + filename, + fileConfig.srcFile.parent, + targetDir); + if (relativeFileName.isNullOrEmpty) { + errorReporter.reportError( + "Failed to find file " + filename + " specified in the" + + " files target property." + ) + } else { + this.targetConfig.filesNamesWithoutPath.add( + relativeFileName + ); + } + } + for (filename : targetConfig.cmakeIncludes) { val relativeCMakeIncludeFileName = - fileConfig.copyFileOrResource( + CUtil.copyFileOrResource( filename, fileConfig.srcFile.parent, targetDir); @@ -1634,8 +1655,8 @@ class CGenerator extends GeneratorBase { * Copy target-specific header file to the src-gen directory. */ def copyTargetHeaderFile() { - fileConfig.copyFileFromClassPath("/lib/c/reactor-c/include/ctarget.h", fileConfig.getSrcGenPath.resolve("ctarget.h")) - fileConfig.copyFileFromClassPath("/lib/c/reactor-c/lib/ctarget.c", fileConfig.getSrcGenPath.resolve("ctarget.c")) + FileUtil.copyFileFromClassPath("/lib/c/reactor-c/include/ctarget.h", fileConfig.getSrcGenPath.resolve("ctarget.h")) + FileUtil.copyFileFromClassPath("/lib/c/reactor-c/lib/ctarget.c", fileConfig.getSrcGenPath.resolve("ctarget.c")) } //////////////////////////////////////////// @@ -4155,7 +4176,7 @@ class CGenerator extends GeneratorBase { // Do this after the above includes so that the preamble can // call built-in functions. code.prComment("Code generated by the Lingua Franca compiler from:") - code.prComment("file:/" +FileConfig.toUnixString(fileConfig.srcFile)) + code.prComment("file:/" + FileUtil.toUnixString(fileConfig.srcFile)) if (this.mainDef !== null) { val mainModel = this.mainDef.reactorClass.toDefinition.eContainer as Model for (p : mainModel.preambles) { diff --git a/org.lflang/src/org/lflang/generator/c/CUtil.java b/org.lflang/src/org/lflang/generator/c/CUtil.java index 0327aab4d2..04c4d8cb44 100644 --- a/org.lflang/src/org/lflang/generator/c/CUtil.java +++ b/org.lflang/src/org/lflang/generator/c/CUtil.java @@ -26,8 +26,14 @@ 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.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -36,19 +42,21 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.FileConfig; import org.lflang.InferredType; import org.lflang.TargetConfig; -import org.lflang.TargetConfig.Mode; import org.lflang.federated.FederateInstance; import org.lflang.generator.GeneratorCommandFactory; +import org.lflang.generator.LFGeneratorContext; import org.lflang.generator.PortInstance; import org.lflang.generator.ReactionInstance; import org.lflang.generator.ReactorInstance; import org.lflang.generator.TriggerInstance; import org.lflang.lf.Parameter; import org.lflang.lf.Port; +import org.lflang.lf.Reactor; import org.lflang.lf.ReactorDecl; import org.lflang.lf.VarRef; import org.lflang.lf.Variable; import org.lflang.lf.WidthTerm; +import org.lflang.util.FileUtil; import org.lflang.util.LFCommand; /** @@ -59,6 +67,18 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY */ public class CUtil { + /** + * Suffix that when appended to the name of a federated reactor yields + * the name of its corresponding RTI executable. + */ + public static final String RTI_BIN_SUFFIX = "_RTI"; + + /** + * Suffix that when appended to the name of a federated reactor yields + * the name of its corresponding distribution script. + */ + public static final String RTI_DISTRIBUTION_SCRIPT_SUFFIX = "_distribute.sh"; + ////////////////////////////////////////////////////// //// Public methods. @@ -532,6 +552,53 @@ static public String triggerRefNested(PortInstance port, String runtimeIndex, St return reactorRefNested(port.getParent(), runtimeIndex, bankIndex) + "." + port.getName() + "_trigger"; } + /** + * Copy the 'fileName' from the 'srcDirectory' to the 'destinationDirectory'. + * This function has a fallback search mechanism, where if `fileName` is not + * found in the `srcDirectory`, it will try to find `fileName` via the following procedure: + * 1- Search in LF_CLASSPATH. @see findFile() + * 2- Search in CLASSPATH. @see findFile() + * 3- Search for 'fileName' as a resource. + * That means the `fileName` can be '/path/to/class/resource'. @see java.lang.Class.getResourceAsStream() + * + * @param fileName Name of the file + * @param srcDir Where the file is currently located + * @param dstDir Where the file should be placed + * @return The name of the file in destinationDirectory + */ + public static String copyFileOrResource(String fileName, Path srcDir, Path dstDir) { + // Try to copy the file from the file system. + Path file = findFile(fileName, srcDir); + if (file != null) { + Path target = dstDir.resolve(file.getFileName()); + try { + Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING); + return file.getFileName().toString(); + } catch (IOException e) { + // Files has failed to copy the file, possibly since + // it doesn't exist. Will try to find the file as a + // resource before giving up. + } + } + + // Try to copy the file as a resource. + // If this is missing, it should have been previously reported as an error. + try { + String filenameWithoutPath = fileName; + int lastSeparator = fileName.lastIndexOf(File.separator); + if (lastSeparator > 0) { + filenameWithoutPath = fileName.substring(lastSeparator + 1); // FIXME: brittle. What if the file is in a subdirectory? + } + FileUtil.copyFileFromClassPath(fileName, dstDir.resolve(filenameWithoutPath)); + return filenameWithoutPath; + } catch (IOException ex) { + // Ignore. Previously reported as a warning. + System.err.println("WARNING: Failed to find file " + fileName); + } + + return ""; + } + ////////////////////////////////////////////////////// //// FIXME: Not clear what the strategy is with the following inner interface. // The {@code ReportCommandErrors} interface allows the @@ -554,6 +621,47 @@ public interface ReportCommandErrors { void report(String errors); } + /** + * Search for a given file name in the given directory. + * If not found, search in directories in LF_CLASSPATH. + * If there is no LF_CLASSPATH environment variable, use CLASSPATH, + * if it is defined. + * The first file found will be returned. + * + * @param fileName The file name or relative path + file name + * in plain string format + * @param directory String representation of the director to search in. + * @return A Java file or null if not found + */ + public static Path findFile(String fileName, Path directory) { + Path foundFile; + + // Check in local directory + foundFile = directory.resolve(fileName); + if (Files.isRegularFile(foundFile)) { + return foundFile; + } + + // Check in LF_CLASSPATH + // Load all the resources in LF_CLASSPATH if it is set. + String classpathLF = System.getenv("LF_CLASSPATH"); + if (classpathLF == null) { + classpathLF = System.getenv("CLASSPATH"); + } + if (classpathLF != null) { + String[] paths = classpathLF.split(System.getProperty("path.separator")); + for (String path : paths) { + foundFile = Paths.get(path).resolve(fileName); + if (Files.isRegularFile(foundFile)) { + return foundFile; + } + } + } + // Not found. + return null; + } + + /** * Run the custom build command specified with the "build" parameter. * This command is executed in the same directory as the source file. @@ -572,7 +680,7 @@ public static void runBuildCommand( GeneratorCommandFactory commandFactory, ErrorReporter errorReporter, ReportCommandErrors reportCommandErrors, - TargetConfig.Mode mode + LFGeneratorContext.Mode mode ) { List commands = getCommands(targetConfig.buildCommands, commandFactory, fileConfig.srcPath); // If the build command could not be found, abort. @@ -581,7 +689,7 @@ public static void runBuildCommand( for (LFCommand cmd : commands) { int returnCode = cmd.run(); - if (returnCode != 0 && mode != Mode.EPOCH) { + if (returnCode != 0 && mode != LFGeneratorContext.Mode.EPOCH) { errorReporter.reportError(String.format( // FIXME: Why is the content of stderr not provided to the user in this error message? "Build command \"%s\" failed with error code %d.", @@ -591,13 +699,52 @@ public static void runBuildCommand( } // For warnings (vs. errors), the return code is 0. // But we still want to mark the IDE. - if (!cmd.getErrors().toString().isEmpty() && mode == Mode.EPOCH) { + if (!cmd.getErrors().toString().isEmpty() && mode == LFGeneratorContext.Mode.EPOCH) { reportCommandErrors.report(cmd.getErrors().toString()); return; // FIXME: Why do we return here? Even if there are warnings, the build process should proceed. } } } + + /** + * Remove files in the bin directory that may have been created. + * Call this if a compilation occurs so that files from a previous + * version do not accidentally get executed. + * @param fileConfig + */ + public static void deleteBinFiles(FileConfig fileConfig) { + String name = FileUtil.nameWithoutExtension(fileConfig.srcFile); + String[] files = fileConfig.binPath.toFile().list(); + List federateNames = new LinkedList<>(); // FIXME: put this in ASTUtils? + fileConfig.resource.getAllContents().forEachRemaining(node -> { + if (node instanceof Reactor) { + Reactor r = (Reactor) node; + if (r.isFederated()) { + r.getInstantiations().forEach(inst -> federateNames + .add(inst.getName())); + } + } + }); + for (String f : files) { + // Delete executable file or launcher script, if any. + // Delete distribution file, if any. + // Delete RTI file, if any. + if (f.equals(name) || f.equals(name + RTI_BIN_SUFFIX) + || f.equals(name + RTI_DISTRIBUTION_SCRIPT_SUFFIX)) { + //noinspection ResultOfMethodCallIgnored + fileConfig.binPath.resolve(f).toFile().delete(); + } + // Delete federate executable files, if any. + for (String federateName : federateNames) { + if (f.equals(name + "_" + federateName)) { + //noinspection ResultOfMethodCallIgnored + fileConfig.binPath.resolve(f).toFile().delete(); + } + } + } + } + ////////////////////////////////////////////////////// //// Private functions. diff --git a/org.lflang/src/org/lflang/generator/cpp/CppCmakeGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppCmakeGenerator.kt index 7ff8043b5b..826addd986 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppCmakeGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppCmakeGenerator.kt @@ -128,6 +128,7 @@ class CppCmakeGenerator(private val targetConfig: TargetConfig, private val file |) |target_include_directories($S{LF_MAIN_TARGET} PUBLIC | "$S{CMAKE_INSTALL_PREFIX}/$S{CMAKE_INSTALL_INCLUDEDIR}" + | "$S{CMAKE_INSTALL_PREFIX}/src" | "$S{PROJECT_SOURCE_DIR}" | "$S{PROJECT_SOURCE_DIR}/__include__" |) diff --git a/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt b/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt index 56d131d5b5..8ca1b41fd0 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppFileConfig.kt @@ -26,16 +26,15 @@ package org.lflang.generator.cpp import org.eclipse.emf.ecore.resource.Resource -import org.eclipse.xtext.generator.IFileSystemAccess2 import org.lflang.FileConfig -import org.lflang.generator.LFGeneratorContext import org.lflang.lf.Reactor import org.lflang.name +import org.lflang.util.FileUtil import java.io.IOException import java.nio.file.Path -class CppFileConfig(resource: Resource, srcGenBasePath: Path, context: LFGeneratorContext) : - FileConfig(resource, srcGenBasePath, context) { +class CppFileConfig(resource: Resource, srcGenBasePath: Path, useHierarchicalBin: Boolean) : + FileConfig(resource, srcGenBasePath, useHierarchicalBin) { /** * Clean any artifacts produced by the C++ code generator. @@ -43,7 +42,7 @@ class CppFileConfig(resource: Resource, srcGenBasePath: Path, context: LFGenerat @Throws(IOException::class) override fun doClean() { super.doClean() - cppBuildDirectories.forEach { deleteDirectory(it) } + cppBuildDirectories.forEach { FileUtil.deleteDirectory(it) } } val cppBuildDirectories = listOf( diff --git a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt index 1fadcb997b..b446c5d6c9 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt @@ -29,7 +29,7 @@ package org.lflang.generator.cpp import org.eclipse.emf.ecore.resource.Resource import org.lflang.ErrorReporter import org.lflang.Target -import org.lflang.TargetConfig.Mode +import org.lflang.generator.LFGeneratorContext.Mode import org.lflang.TargetProperty import org.lflang.TimeUnit import org.lflang.TimeValue @@ -37,7 +37,6 @@ import org.lflang.generator.CodeMap import org.lflang.generator.GeneratorBase import org.lflang.generator.GeneratorResult import org.lflang.generator.IntegratedBuilder -import org.lflang.generator.JavaGeneratorUtils import org.lflang.generator.LFGeneratorContext import org.lflang.generator.TargetTypes import org.lflang.generator.canGenerate @@ -47,6 +46,7 @@ import org.lflang.lf.VarRef import org.lflang.scoping.LFGlobalScopeProvider import org.lflang.toDefinition import org.lflang.toUnixString +import org.lflang.util.FileUtil import org.lflang.util.LFCommand import java.nio.file.Files import java.nio.file.Path @@ -127,9 +127,17 @@ class CppGenerator( // copy static library files over to the src-gen directory val genIncludeDir = srcGenPath.resolve("__include__") - fileConfig.copyFileFromClassPath("$libDir/lfutil.hh", genIncludeDir.resolve("lfutil.hh"), true) - fileConfig.copyFileFromClassPath("$libDir/time_parser.hh", genIncludeDir.resolve("time_parser.hh"), true) - fileConfig.copyFileFromClassPath( + FileUtil.copyFileFromClassPath( + "$libDir/lfutil.hh", + genIncludeDir.resolve("lfutil.hh"), + true + ) + FileUtil.copyFileFromClassPath( + "$libDir/time_parser.hh", + genIncludeDir.resolve("time_parser.hh"), + true + ) + FileUtil.copyFileFromClassPath( "$libDir/3rd-party/cxxopts.hpp", genIncludeDir.resolve("CLI").resolve("cxxopts.hpp"), true @@ -140,7 +148,7 @@ class CppGenerator( if (targetConfig.runtimeVersion != null) { fetchReactorCpp() } else { - fileConfig.copyDirectoryFromClassPath( + FileUtil.copyDirectoryFromClassPath( "$libDir/reactor-cpp", fileConfig.srcGenBasePath.resolve("reactor-cpp-default"), true @@ -157,7 +165,7 @@ class CppGenerator( val mainCodeMap = CodeMap.fromGeneratedCode(CppMainGenerator(mainReactor, targetConfig, cppFileConfig).generateCode()) cppSources.add(mainFile) codeMaps[srcGenPath.resolve(mainFile)] = mainCodeMap - JavaGeneratorUtils.writeToFile(mainCodeMap.generatedCode, srcGenPath.resolve(mainFile), true) + FileUtil.writeToFile(mainCodeMap.generatedCode, srcGenPath.resolve(mainFile), true) // generate header and source files for all reactors for (r in reactors) { @@ -171,8 +179,16 @@ class CppGenerator( val headerCodeMap = CodeMap.fromGeneratedCode(generator.generateHeader()) codeMaps[srcGenPath.resolve(headerFile)] = headerCodeMap - JavaGeneratorUtils.writeToFile(headerCodeMap.generatedCode, srcGenPath.resolve(headerFile), true) - JavaGeneratorUtils.writeToFile(reactorCodeMap.generatedCode, srcGenPath.resolve(sourceFile), true) + FileUtil.writeToFile( + headerCodeMap.generatedCode, + srcGenPath.resolve(headerFile), + true + ) + FileUtil.writeToFile( + reactorCodeMap.generatedCode, + srcGenPath.resolve(sourceFile), + true + ) } // generate file level preambles for all resources @@ -186,19 +202,39 @@ class CppGenerator( val headerCodeMap = CodeMap.fromGeneratedCode(generator.generateHeader()) codeMaps[srcGenPath.resolve(headerFile)] = headerCodeMap - JavaGeneratorUtils.writeToFile(headerCodeMap.generatedCode, srcGenPath.resolve(headerFile), true) - JavaGeneratorUtils.writeToFile(preambleCodeMap.generatedCode, srcGenPath.resolve(sourceFile), true) + FileUtil.writeToFile( + headerCodeMap.generatedCode, + srcGenPath.resolve(headerFile), + true + ) + FileUtil.writeToFile( + preambleCodeMap.generatedCode, + srcGenPath.resolve(sourceFile), + true + ) } // generate the cmake scripts val cmakeGenerator = CppCmakeGenerator(targetConfig, cppFileConfig) val srcGenRoot = fileConfig.srcGenBasePath val pkgName = fileConfig.srcGenPkgPath.fileName.toString() - JavaGeneratorUtils.writeToFile(cmakeGenerator.generateRootCmake(pkgName), srcGenRoot.resolve("CMakeLists.txt"), true) - JavaGeneratorUtils.writeToFile(cmakeGenerator.generateCmake(cppSources), srcGenPath.resolve("CMakeLists.txt"), true) + FileUtil.writeToFile( + cmakeGenerator.generateRootCmake(pkgName), + srcGenRoot.resolve("CMakeLists.txt"), + true + ) + FileUtil.writeToFile( + cmakeGenerator.generateCmake(cppSources), + srcGenPath.resolve("CMakeLists.txt"), + true + ) var subdir = srcGenPath.parent while (subdir != srcGenRoot) { - JavaGeneratorUtils.writeToFile(cmakeGenerator.generateSubdirCmake(), subdir.resolve("CMakeLists.txt"), true) + FileUtil.writeToFile( + cmakeGenerator.generateSubdirCmake(), + subdir.resolve("CMakeLists.txt"), + true + ) subdir = subdir.parent } diff --git a/org.lflang/src/org/lflang/generator/python/PyUtil.java b/org.lflang/src/org/lflang/generator/python/PyUtil.java index ff65be5ce6..b75cabf960 100644 --- a/org.lflang/src/org/lflang/generator/python/PyUtil.java +++ b/org.lflang/src/org/lflang/generator/python/PyUtil.java @@ -32,9 +32,11 @@ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY import org.lflang.lf.Value; import java.io.IOException; +import java.nio.file.Path; import org.lflang.ASTUtils; import org.lflang.FileConfig; +import org.lflang.util.FileUtil; /** @@ -175,17 +177,18 @@ protected static String getPythonTargetValue(Value v) { public static void copyTargetFiles(FileConfig fileConfig) throws IOException { // Copy the required target language files into the target file system. // This will also overwrite previous versions. - fileConfig.copyFileFromClassPath( + final Path srcGen = fileConfig.getSrcGenPath(); + FileUtil.copyFileFromClassPath( "/lib/py/reactor-c-py/include/pythontarget.h", - fileConfig.getSrcGenPath().resolve("pythontarget.h") + srcGen.resolve("pythontarget.h") ); - fileConfig.copyFileFromClassPath( + FileUtil.copyFileFromClassPath( "/lib/py/reactor-c-py/lib/pythontarget.c", - fileConfig.getSrcGenPath().resolve("pythontarget.c") + srcGen.resolve("pythontarget.c") ); - fileConfig.copyFileFromClassPath( + FileUtil.copyFileFromClassPath( "/lib/c/reactor-c/include/ctarget.h", - fileConfig.getSrcGenPath().resolve("ctarget.h") + srcGen.resolve("ctarget.h") ); } } \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/python/PythonGenerator.java b/org.lflang/src/org/lflang/generator/python/PythonGenerator.java index 4c6cf4be18..e52db93876 100644 --- a/org.lflang/src/org/lflang/generator/python/PythonGenerator.java +++ b/org.lflang/src/org/lflang/generator/python/PythonGenerator.java @@ -50,7 +50,6 @@ import org.lflang.InferredType; import org.lflang.JavaAstUtils; import org.lflang.Target; -import org.lflang.TargetConfig.Mode; import org.lflang.federated.FedFileConfig; import org.lflang.federated.FederateInstance; import org.lflang.federated.launcher.FedPyLauncher; @@ -59,8 +58,8 @@ import org.lflang.generator.CodeBuilder; import org.lflang.generator.CodeMap; import org.lflang.generator.GeneratorResult; +import org.lflang.generator.GeneratorUtils; import org.lflang.generator.IntegratedBuilder; -import org.lflang.generator.JavaGeneratorUtils; import org.lflang.generator.LFGeneratorContext; import org.lflang.generator.ReactorInstance; import org.lflang.generator.SubContext; @@ -78,6 +77,7 @@ import org.lflang.lf.Reactor; import org.lflang.lf.ReactorDecl; import org.lflang.lf.VarRef; +import org.lflang.util.FileUtil; import org.lflang.util.LFCommand; import com.google.common.base.Objects; @@ -176,14 +176,6 @@ public Target getTarget() { // ////////////////////////////////////////// // // Public methods - @Override - public String printInfo() { - System.out.println("Generating code for: " + fileConfig.resource.getURI().toString()); - System.out.println("******** Mode: " + fileConfig.context.getMode()); - System.out.println("******** Generated sources: " + fileConfig.getSrcGenPath()); - return null; - } - @Override public TargetTypes getTargetTypes() { return types; @@ -262,7 +254,7 @@ public String generatePythonSetupFile() { sources.add(topLevelName + ".c"); sources = sources.stream() .map(Paths::get) - .map(FileConfig::toUnixString) + .map(FileUtil::toUnixString) .map(PythonGenerator::addDoubleQuotes) .collect(Collectors.toList()); @@ -308,7 +300,7 @@ public Map generatePythonFiles(FederateInstance federate) throws } Map codeMaps = new HashMap<>(); codeMaps.put(filePath, CodeMap.fromGeneratedCode(generatePythonCode(federate).toString())); - JavaGeneratorUtils.writeToFile(codeMaps.get(filePath).getGeneratedCode(), filePath); + FileUtil.writeToFile(codeMaps.get(filePath).getGeneratedCode(), filePath); Path setupPath = fileConfig.getSrcGenPath().resolve("setup.py"); // Handle Python setup @@ -316,7 +308,7 @@ public Map generatePythonFiles(FederateInstance federate) throws Files.deleteIfExists(setupPath); // Create the setup file - JavaGeneratorUtils.writeToFile(generatePythonSetupFile(), setupPath); + FileUtil.writeToFile(generatePythonSetupFile(), setupPath); return codeMaps; } @@ -641,7 +633,7 @@ public void includeTargetLanguageHeaders() { */ @Override public boolean isOSCompatible() { - if (JavaGeneratorUtils.isHostWindows() && isFederated) { + if (GeneratorUtils.isHostWindows() && isFederated) { errorReporter.reportError( "Federated LF programs with a Python target are currently not supported on Windows. Exiting code generation." ); @@ -712,7 +704,7 @@ public void doGenerate(Resource resource, LFGeneratorContext context) { ); // If there are no federates, compile and install the generated code new PythonValidator(fileConfig, errorReporter, codeMaps, protoNames).doValidate(context); - if (!errorsOccurred() && !Objects.equal(context.getMode(), Mode.LSP_MEDIUM)) { + if (!errorsOccurred() && !Objects.equal(context.getMode(), LFGeneratorContext.Mode.LSP_MEDIUM)) { compilingFederatesContext.reportProgress( String.format("Validation complete. Compiling and installing %d/%d Python modules...", federateCount, federates.size()), diff --git a/org.lflang/src/org/lflang/generator/rust/RustEmitter.kt b/org.lflang/src/org/lflang/generator/rust/RustEmitter.kt index b9339986f4..6e14238127 100644 --- a/org.lflang/src/org/lflang/generator/rust/RustEmitter.kt +++ b/org.lflang/src/org/lflang/generator/rust/RustEmitter.kt @@ -24,10 +24,10 @@ package org.lflang.generator.rust -import org.lflang.FileConfig import org.lflang.generator.CodeMap import org.lflang.generator.PrependOperator import org.lflang.generator.rust.RustEmitter.generateRustProject +import org.lflang.util.FileUtil import java.nio.file.Files import java.nio.file.Path @@ -77,9 +77,9 @@ object RustEmitter : RustEmitterBase() { for (modPath in gen.crate.modulesToIncludeInMain) { val target = fileConfig.srcGenPath.resolve("src").resolve(modPath.fileName) if (Files.isDirectory(modPath)) { - FileConfig.copyDirectory(modPath, target) + FileUtil.copyDirectory(modPath, target) } else { - FileConfig.copyFile(modPath, target) + FileUtil.copyFile(modPath, target) } } return codeMaps diff --git a/org.lflang/src/org/lflang/generator/rust/RustFileConfig.kt b/org.lflang/src/org/lflang/generator/rust/RustFileConfig.kt index afe96b3667..95e287d005 100644 --- a/org.lflang/src/org/lflang/generator/rust/RustFileConfig.kt +++ b/org.lflang/src/org/lflang/generator/rust/RustFileConfig.kt @@ -27,15 +27,15 @@ package org.lflang.generator.rust import org.eclipse.emf.ecore.resource.Resource import org.lflang.FileConfig import org.lflang.generator.CodeMap -import org.lflang.generator.LFGeneratorContext +import org.lflang.util.FileUtil import java.io.Closeable import java.io.IOException import java.nio.file.Files import java.nio.file.Path import kotlin.system.measureTimeMillis -class RustFileConfig(resource: Resource, srcGenBasePath: Path, context: LFGeneratorContext) : - FileConfig(resource, srcGenBasePath, context) { +class RustFileConfig(resource: Resource, srcGenBasePath: Path, useHierarchicalBin: Boolean) : + FileConfig(resource, srcGenBasePath, useHierarchicalBin) { /** * Clean any artifacts produced by the C++ code generator. @@ -43,7 +43,7 @@ class RustFileConfig(resource: Resource, srcGenBasePath: Path, context: LFGenera @Throws(IOException::class) override fun doClean() { super.doClean() - deleteDirectory(outPath.resolve("target")) + FileUtil.deleteDirectory(outPath.resolve("target")) } inline fun emit(codeMaps: MutableMap, p: Path, f: Emitter.() -> Unit) { @@ -52,9 +52,8 @@ class RustFileConfig(resource: Resource, srcGenBasePath: Path, context: LFGenera } } - inline fun emit(codeMaps: MutableMap, pathRelativeToOutDir: String, f: Emitter.() -> Unit): Unit - = emit(codeMaps, srcGenPath.resolve(pathRelativeToOutDir), f) - + inline fun emit(codeMaps: MutableMap, pathRelativeToOutDir: String, f: Emitter.() -> Unit): Unit = + emit(codeMaps, getSrcGenPath().resolve(pathRelativeToOutDir), f) } /** diff --git a/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt b/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt index b2a100ecb0..283e5fa1a4 100644 --- a/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt +++ b/org.lflang/src/org/lflang/generator/rust/RustGenerator.kt @@ -25,10 +25,8 @@ package org.lflang.generator.rust import org.eclipse.emf.ecore.resource.Resource -import org.eclipse.xtext.generator.IFileSystemAccess2 import org.lflang.ErrorReporter import org.lflang.Target -import org.lflang.TargetConfig import org.lflang.TargetProperty.BuildType import org.lflang.generator.canGenerate import org.lflang.generator.CodeMap @@ -85,7 +83,7 @@ class RustGenerator( ) val exec = fileConfig.binPath.toAbsolutePath().resolve(gen.executableName) Files.deleteIfExists(exec) // cleanup, cargo doesn't do it - if (context.mode == TargetConfig.Mode.LSP_MEDIUM) RustValidator(fileConfig, errorReporter, codeMaps).doValidate(context) + if (context.mode == LFGeneratorContext.Mode.LSP_MEDIUM) RustValidator(fileConfig, errorReporter, codeMaps).doValidate(context) else invokeRustCompiler(context, gen.executableName, codeMaps) } } diff --git a/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt b/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt index 5a0fd7a01b..fd844bf912 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt @@ -27,7 +27,7 @@ package org.lflang.generator.ts import org.eclipse.emf.ecore.resource.Resource import org.lflang.FileConfig -import org.lflang.generator.LFGeneratorContext +import org.lflang.util.FileUtil import java.io.IOException import java.nio.file.Path @@ -41,8 +41,8 @@ import java.nio.file.Path * @author {Hokeun Kim } */ class TSFileConfig( - resource: Resource, srcGenBasePath: Path, context: LFGeneratorContext -) : FileConfig(resource, srcGenBasePath, context) { + resource: Resource, srcGenBasePath: Path, useHierarchicalBin: Boolean +) : FileConfig(resource, srcGenBasePath, useHierarchicalBin) { /** * Clean any artifacts produced by the TypeScript code generator. @@ -50,7 +50,7 @@ class TSFileConfig( @Throws(IOException::class) override fun doClean() { super.doClean() - deleteDirectory(srcGenPath) + FileUtil.deleteDirectory(srcGenPath) } /** diff --git a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt index 3fe2fd783d..df510f29cd 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt @@ -28,14 +28,25 @@ package org.lflang.generator.ts import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.util.CancelIndicator import org.lflang.ErrorReporter -import org.lflang.inferredType import org.lflang.InferredType -import org.lflang.TimeValue import org.lflang.JavaAstUtils import org.lflang.Target -import org.lflang.TargetConfig.Mode -import org.lflang.federated.launcher.FedTSLauncher +import org.lflang.TimeValue import org.lflang.federated.FederateInstance +import org.lflang.federated.launcher.FedTSLauncher +import org.lflang.federated.serialization.SupportedSerializers +import org.lflang.generator.CodeMap +import org.lflang.generator.GeneratorBase +import org.lflang.generator.GeneratorResult +import org.lflang.generator.IntegratedBuilder +import org.lflang.generator.GeneratorUtils +import org.lflang.generator.LFGeneratorContext +import org.lflang.generator.PrependOperator +import org.lflang.generator.SubContext +import org.lflang.generator.TargetTypes +import org.lflang.generator.ValueGenerator +import org.lflang.generator.canGenerate +import org.lflang.inferredType import org.lflang.lf.Action import org.lflang.lf.Delay import org.lflang.lf.Instantiation @@ -45,23 +56,11 @@ import org.lflang.lf.Type import org.lflang.lf.Value import org.lflang.lf.VarRef import org.lflang.scoping.LFGlobalScopeProvider +import org.lflang.util.FileUtil import java.nio.file.Files -import java.util.LinkedList -import org.lflang.federated.serialization.SupportedSerializers -import org.lflang.generator.canGenerate -import org.lflang.generator.CodeMap -import org.lflang.generator.GeneratorBase -import org.lflang.generator.GeneratorResult -import org.lflang.generator.IntegratedBuilder -import org.lflang.generator.JavaGeneratorUtils -import org.lflang.generator.LFGeneratorContext -import org.lflang.generator.PrependOperator -import org.lflang.generator.TargetTypes -import org.lflang.generator.ValueGenerator -import org.lflang.generator.SubContext import java.nio.file.Path import java.nio.file.StandardCopyOption -import kotlin.collections.HashMap +import java.util.* private const val NO_NPM_MESSAGE = "The TypeScript target requires npm >= 6.14.4. " + "For installation instructions, see: https://www.npmjs.com/get-npm. \n" + @@ -186,7 +185,7 @@ class TSGenerator( !context.cancelIndicator.isCanceled && passesChecks(TSValidator(tsFileConfig, errorReporter, codeMaps), parsingContext) ) { - if (context.mode == Mode.LSP_MEDIUM) { + if (context.mode == LFGeneratorContext.Mode.LSP_MEDIUM) { context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(codeMaps)) } else { compile(resource, parsingContext) @@ -203,7 +202,9 @@ class TSGenerator( */ private fun clean(context: LFGeneratorContext) { // Dirty shortcut for integrated mode: Delete nothing, saving the node_modules directory to avoid re-running pnpm. - if (context.mode != Mode.LSP_MEDIUM) fileConfig.deleteDirectory(fileConfig.srcGenPath) + if (context.mode != LFGeneratorContext.Mode.LSP_MEDIUM) FileUtil.deleteDirectory( + fileConfig.srcGenPath + ) } /** @@ -211,7 +212,7 @@ class TSGenerator( */ private fun copyRuntime() { for (runtimeFile in RUNTIME_FILES) { - fileConfig.copyFileFromClassPath( + FileUtil.copyFileFromClassPath( "$LIB_PATH/reactor-ts/src/core/$runtimeFile", tsFileConfig.tsCoreGenPath().resolve(runtimeFile) ) @@ -234,7 +235,7 @@ class TSGenerator( "No '" + configFile + "' exists in " + fileConfig.srcPath + ". Using default configuration." ) - fileConfig.copyFileFromClassPath("$LIB_PATH/$configFile", configFileDest) + FileUtil.copyFileFromClassPath("$LIB_PATH/$configFile", configFileDest) } } } @@ -269,7 +270,7 @@ class TSGenerator( tsCode.append(reactorGenerator.generateReactorInstanceAndStart(this.mainDef, mainParameters)) val codeMap = CodeMap.fromGeneratedCode(tsCode.toString()) codeMaps[tsFilePath] = codeMap - JavaGeneratorUtils.writeToFile(codeMap.generatedCode, tsFilePath) + FileUtil.writeToFile(codeMap.generatedCode, tsFilePath) if (targetConfig.dockerOptions != null && isFederated) { println("WARNING: Federated Docker file generation is not supported on the Typescript target. No docker file is generated.") @@ -278,14 +279,17 @@ class TSGenerator( val dockerComposeFile = fileConfig.srcGenPath.resolve("docker-compose.yml") val dockerGenerator = TSDockerGenerator(tsFileName) println("docker file written to $dockerFilePath") - JavaGeneratorUtils.writeToFile(dockerGenerator.generateDockerFileContent(), dockerFilePath) - JavaGeneratorUtils.writeToFile(dockerGenerator.generateDockerComposeFileContent(), dockerComposeFile) + FileUtil.writeToFile(dockerGenerator.generateDockerFileContent(), dockerFilePath) + FileUtil.writeToFile( + dockerGenerator.generateDockerComposeFileContent(), + dockerComposeFile + ) } } private fun compile(resource: Resource, parsingContext: LFGeneratorContext) { - JavaGeneratorUtils.refreshProject(resource, parsingContext.mode) + GeneratorUtils.refreshProject(resource, parsingContext.mode) if (parsingContext.cancelIndicator.isCanceled) return parsingContext.reportProgress("Transpiling to JavaScript...", 70) @@ -301,8 +305,8 @@ class TSGenerator( /** * Return whether it is advisable to install dependencies. */ - private fun shouldCollectDependencies(context: LFGeneratorContext): Boolean - = context.mode != Mode.LSP_MEDIUM || !fileConfig.srcGenPkgPath.resolve("node_modules").toFile().exists() + private fun shouldCollectDependencies(context: LFGeneratorContext): Boolean = + context.mode != LFGeneratorContext.Mode.LSP_MEDIUM || !fileConfig.srcGenPkgPath.resolve("node_modules").toFile().exists() /** * Collect the dependencies in package.json and their @@ -327,7 +331,8 @@ class TSGenerator( val ret = pnpmInstall.run(context.cancelIndicator) if (ret != 0) { val errors: String = pnpmInstall.errors.toString() - errorReporter.reportError(JavaGeneratorUtils.findTarget(resource), + errorReporter.reportError( + GeneratorUtils.findTarget(resource), "ERROR: pnpm install command failed" + if (errors.isBlank()) "." else ":\n$errors") } } else { @@ -343,10 +348,10 @@ class TSGenerator( if (npmInstall.run(context.cancelIndicator) != 0) { errorReporter.reportError( - JavaGeneratorUtils.findTarget(resource), + GeneratorUtils.findTarget(resource), "ERROR: npm install command failed: " + npmInstall.errors.toString()) errorReporter.reportError( - JavaGeneratorUtils.findTarget(resource), "ERROR: npm install command failed." + + GeneratorUtils.findTarget(resource), "ERROR: npm install command failed." + "\nFor installation instructions, see: https://www.npmjs.com/get-npm") return } @@ -474,7 +479,7 @@ class TSGenerator( } private fun isOsCompatible(): Boolean { - if (isFederated && JavaGeneratorUtils.isHostWindows()) { + if (isFederated && GeneratorUtils.isHostWindows()) { errorReporter.reportError( "Federated LF programs with a TypeScript target are currently not supported on Windows. Exiting code generation." ) diff --git a/org.lflang/src/org/lflang/util/FileUtil.java b/org.lflang/src/org/lflang/util/FileUtil.java new file mode 100644 index 0000000000..3cb897fe1e --- /dev/null +++ b/org.lflang/src/org/lflang/util/FileUtil.java @@ -0,0 +1,424 @@ +package org.lflang.util; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.IPath; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.util.RuntimeIOException; + +import org.lflang.FileConfig; + +public class FileUtil { + + /** + * Return the name of the file excluding its file extension. + * @param file A Path object + * @return The name of the file excluding its file extension. + */ + public static String nameWithoutExtension(Path file) { + String name = file.getFileName().toString(); + int idx = name.lastIndexOf('.'); + return idx < 0 ? name : name.substring(0, idx); + } + + /** + * Return the name of the file associated with the given resource, + * excluding its file extension. + * @param r Any {@code Resource}. + * @return The name of the file associated with the given resource, + * excluding its file extension. + * @throws IOException If the resource has an invalid URI. + */ + public static String nameWithoutExtension(Resource r) throws IOException { + return nameWithoutExtension(toPath(r)); + } + + /** + * Return a java.nio.Path object corresponding to the given URI. + * @throws IOException If the given URI is invalid. + */ + public static Path toPath(URI uri) throws IOException { + return Paths.get(toIPath(uri).toFile().getAbsolutePath()); + } + + /** + * Return a java.nio.Path object corresponding to the given Resource. + * @throws IOException If the given resource has an invalid URI. + */ + public static Path toPath(Resource resource) throws IOException { + return toPath(resource.getURI()); + } + + /** + * Return an org.eclipse.core.runtime.Path object corresponding to the + * given URI. + * @throws IOException If the given URI is invalid. + */ + public static IPath toIPath(URI uri) throws IOException { + if (uri.isPlatform()) { + IPath path = new org.eclipse.core.runtime.Path(uri.toPlatformString(true)); + if (path.segmentCount() == 1) { + return ResourcesPlugin.getWorkspace().getRoot().getProject(path.lastSegment()).getLocation(); + } else { + return ResourcesPlugin.getWorkspace().getRoot().getFile(path).getLocation(); + } + } else if (uri.isFile()) { + return new org.eclipse.core.runtime.Path(uri.toFileString()); + } else { + throw new IOException("Unrecognized file protocol in URI " + uri); + } + } + + /** + * Convert a given path to a unix-style string. + * + * This ensures that '/' is used instead of '\' as file separator. + */ + public static String toUnixString(Path path) { + return path.toString().replace('\\', '/'); + } + + /** + * Recursively copies the contents of the given 'src' + * directory to 'dest'. Existing files of the destination + * may be overwritten. + * + * @param src The source directory path. + * @param dest The destination directory path. + * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed + * @throws IOException if copy fails. + */ + public static void copyDirectory(final Path src, final Path dest, final boolean skipIfUnchanged) throws IOException { + try (Stream stream = Files.walk(src)) { + stream.forEach(source -> { + // Handling checked exceptions in lambda expressions is + // hard. See + // https://www.baeldung.com/java-lambda-exceptions#handling-checked-exceptions. + // An alternative would be to create a custom Consumer interface and use that + // here. + if (Files.isRegularFile(source)) { // do not copy directories + try { + Path target = dest.resolve(src.relativize(source)); + Files.createDirectories(target.getParent()); + copyFile(source, target, skipIfUnchanged); + } catch (IOException e) { + throw new RuntimeIOException(e); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + } + } + + /** + * Recursively copies the contents of the given 'src' + * directory to 'dest'. Existing files of the destination + * may be overwritten. + * + * @param src The source directory path. + * @param dest The destination directory path. + * @throws IOException if copy fails. + */ + public static void copyDirectory(final Path src, final Path dest) throws IOException { + copyDirectory(src, dest, false); + } + + /** + * Copy a given file from 'source' to 'destination'. + * + * This also creates new directories for any directories on the destination + * path that do not yet exist. + * + * @param source The source file path. + * @param destination The destination file path. + * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed + * @throws IOException if copy fails. + */ + public static void copyFile(Path source, Path destination, boolean skipIfUnchanged) throws IOException { + BufferedInputStream stream = new BufferedInputStream(new FileInputStream(source.toFile())); + try (stream) { + copyInputStream(stream, destination, skipIfUnchanged); + } + } + + /** + * Copy a given file from 'source' to 'destination'. + * + * This also creates new directories for any directories on the destination + * path that do not yet exist. + * + * @param source The source file path. + * @param destination The destination file path. + * @throws IOException if copy fails. + */ + public static void copyFile(Path source, Path destination) throws IOException { + copyFile(source, destination, false); + } + + /** + * Copy a given input stream to a destination file. + * + * This also creates new directories for any directories on the destination + * path that do not yet exist. + * + * @param source The source input stream. + * @param destination The destination file path. + * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed + * @throws IOException if copy fails. + */ + private static void copyInputStream(InputStream source, Path destination, boolean skipIfUnchanged) throws IOException { + Files.createDirectories(destination.getParent()); + if(skipIfUnchanged && Files.isRegularFile(destination)) { + if (Arrays.equals(source.readAllBytes(), Files.readAllBytes(destination))) { + return; + } + } + Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING); + } + + /** + * Lookup a file in the classpath and copy its contents to a destination path + * in the filesystem. + * + * This also creates new directories for any directories on the destination + * path that do not yet exist. + * + * @param source The source file as a path relative to the classpath. + * @param destination The file system path that the source file is copied to. + * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed + * @throws IOException If the given source cannot be copied. + */ + public static void copyFileFromClassPath(final String source, final Path destination, final boolean skipIfUnchanged) throws IOException { + InputStream sourceStream = FileConfig.class.getResourceAsStream(source); + + // Copy the file. + if (sourceStream == null) { + throw new IOException( + "A required target resource could not be found: " + source + "\n" + + "Perhaps a git submodule is missing or not up to date.\n" + + "See https://github.com/icyphy/lingua-franca/wiki/downloading-and-building#clone-the-lingua-franca-repository.\n" + + + "Also try to refresh and clean the project explorer if working from eclipse."); + } else { + try (sourceStream) { + copyInputStream(sourceStream, destination, skipIfUnchanged); + } + } + } + + /** + * Lookup a file in the classpath and copy its contents to a destination path + * in the filesystem. + * + * This also creates new directories for any directories on the destination + * path that do not yet exist. + * + * @param source The source file as a path relative to the classpath. + * @param destination The file system path that the source file is copied to. + * @throws IOException If the given source cannot be copied. + */ + public static void copyFileFromClassPath(final String source, final Path destination) throws IOException { + copyFileFromClassPath(source, destination, false); + } + + /** + * Lookup a directory in the classpath and copy its contents to a destination path + * in the filesystem. + * + * This also creates new directories for any directories on the destination + * path that do not yet exist. + * + * @param source The source directory as a path relative to the classpath. + * @param destination The file system path that the source directory is copied to. + * @param skipIfUnchanged If true, don't overwrite the file if its content would not be changed + * @throws IOException If the given source cannot be copied. + */ + public static void copyDirectoryFromClassPath(final String source, final Path destination, final boolean skipIfUnchanged) throws IOException { + final URL resource = FileConfig.class.getResource(source); + if (resource == null) { + throw new IOException( + "A required target resource could not be found: " + source + "\n" + + "Perhaps a git submodule is missing or not up to date.\n" + + "See https://github.com/icyphy/lingua-franca/wiki/downloading-and-building#clone-the-lingua-franca-repository.\n" + + + "Also try to refresh and clean the project explorer if working from eclipse."); + } + + final URLConnection connection = resource.openConnection(); + if (connection instanceof JarURLConnection) { + copyDirectoryFromJar((JarURLConnection) connection, destination, skipIfUnchanged); + } else { + try { + Path dir = Paths.get(FileLocator.toFileURL(resource).toURI()); + copyDirectory(dir, destination, skipIfUnchanged); + } catch(URISyntaxException e) { + // This should never happen as toFileURL should always return a valid URL + throw new IOException("Unexpected error while resolving " + source + " on the classpath"); + } + } + } + + /** + * Copy a directory from ta jar to a destination path in the filesystem. + * + * This method should only be used in standalone mode (lfc). + * + * This also creates new directories for any directories on the destination + * path that do not yet exist + * + * @param connection a URLConnection to the source directory within the jar + * @param destination The file system path that the source directory is copied to. + * @param skipIfUnchanged If true, don't overwrite the file if its content would not be changed + * @throws IOException If the given source cannot be copied. + */ + private static void copyDirectoryFromJar(JarURLConnection connection, final Path destination, final boolean skipIfUnchanged) throws IOException { + final JarFile jar = connection.getJarFile(); + final String connectionEntryName = connection.getEntryName(); + + // Iterate all entries in the jar file. + for (Enumeration e = jar.entries(); e.hasMoreElements(); ) { + final JarEntry entry = e.nextElement(); + final String entryName = entry.getName(); + + // Extract files only if they match the given source path. + if (entryName.startsWith(connectionEntryName)) { + String filename = entry.getName().substring(connectionEntryName.length() + 1); + Path currentFile = destination.resolve(filename); + + if (entry.isDirectory()) { + Files.createDirectories(currentFile); + } else { + InputStream is = jar.getInputStream(entry); + try (is) { + copyInputStream(is, currentFile, skipIfUnchanged); + } + } + } + } + } + + /** + * Copy a list of files from a given source directory to a given destination directory. + * @param srcDir The directory to copy files from. + * @param dstDir The directory to copy files to. + * @param files The list of files to copy. + * @throws IOException If any of the given files cannot be copied. + */ + public static void copyFilesFromClassPath(String srcDir, Path dstDir, List files) throws IOException { + for (String file : files) { + copyFileFromClassPath(srcDir + '/' + file, dstDir.resolve(file)); + } + } + + /** + * Recursively delete a directory if it exists. + * + * @throws IOException If an I/O error occurs. + */ + public static void deleteDirectory(Path dir) throws IOException { + if (Files.isDirectory(dir)) { + System.out.println("Cleaning " + dir); + List pathsToDelete = Files.walk(dir) + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + for (Path path : pathsToDelete) { + Files.deleteIfExists(path); + } + } + } + + /** + * Get the iResource corresponding to the provided resource if it can be + * found. + */ + public static IResource getIResource(Resource r) { + return getIResource(java.net.URI.create(r.getURI().toString())); + } + + /** + * Get the specified path as an Eclipse IResource or null if it is not found. + */ + public static IResource getIResource(Path path) { + return getIResource(path.toUri()); + } + + /** + * Get the specified uri as an Eclipse IResource or null if it is not found. + * + * @param uri A java.net.uri of the form "file://path". + */ + public static IResource getIResource(java.net.URI uri) { + IResource resource = null; + // For some peculiar reason known only to Eclipse developers, + // the resource cannot be used directly but has to be converted + // a resource relative to the workspace root. + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + + IFile[] files = workspaceRoot.findFilesForLocationURI(uri); + if (files != null && files.length > 0 && files[0] != null) { + resource = files[0]; + } + return resource; + } + + /** + * Write text to a file. + * @param text The text to be written. + * @param path The file to write the code to. + * @param skipIfUnchanged If true, don't overwrite the destination file if its content would not be changed + */ + public static void writeToFile(String text, Path path, boolean skipIfUnchanged) throws IOException { + Files.createDirectories(path.getParent()); + final byte[] bytes = text.getBytes(); + if (skipIfUnchanged && Files.isRegularFile(path)) { + if (Arrays.equals(bytes, Files.readAllBytes(path))) { + return; + } + } + Files.write(path, text.getBytes()); + } + + /** + * Write text to a file. + * @param text The text to be written. + * @param path The file to write the code to. + */ + public static void writeToFile(String text, Path path) throws IOException { + writeToFile(text, path, false); + } + + /** + * Write text to a file. + * @param text The text to be written. + * @param path The file to write the code to. + */ + public static void writeToFile(CharSequence text, Path path) throws IOException { + writeToFile(text.toString(), path, false); + } +} diff --git a/org.lflang/src/org/lflang/validation/LFValidator.java b/org.lflang/src/org/lflang/validation/LFValidator.java index a655db04e8..d66f10ebb5 100644 --- a/org.lflang/src/org/lflang/validation/LFValidator.java +++ b/org.lflang/src/org/lflang/validation/LFValidator.java @@ -56,7 +56,6 @@ import org.eclipse.xtext.validation.CheckType; import org.eclipse.xtext.validation.ValidationMessageAcceptor; import org.lflang.ASTUtils; -import org.lflang.FileConfig; import org.lflang.JavaAstUtils; import org.lflang.ModelInfo; import org.lflang.Target; @@ -104,6 +103,7 @@ import org.lflang.lf.Visibility; import org.lflang.lf.WidthSpec; import org.lflang.lf.WidthTerm; +import org.lflang.util.FileUtil; import com.google.inject.Inject; @@ -850,7 +850,7 @@ public void checkReactor(Reactor reactor) throws IOException { // Continue checks, but without any superclasses. superClasses = new LinkedHashSet<>(); } - String name = FileConfig.nameWithoutExtension(reactor.eResource()); + String name = FileUtil.nameWithoutExtension(reactor.eResource()); if (reactor.getName() == null) { if (!reactor.isFederated() && !reactor.isMain()) { error( @@ -1048,7 +1048,7 @@ public void checkTargetDecl(TargetDecl target) throws IOException { } else { this.target = targetOpt.get(); } - String lfFileName = FileConfig.nameWithoutExtension(target.eResource()); + String lfFileName = FileUtil.nameWithoutExtension(target.eResource()); if (Character.isDigit(lfFileName.charAt(0))) { errorReporter.reportError("LF file names must not start with a number"); } diff --git a/test/C/src/Minimal.lf b/test/C/src/Minimal.lf index 76de0c5b9e..3fbd9cc948 100644 --- a/test/C/src/Minimal.lf +++ b/test/C/src/Minimal.lf @@ -5,4 +5,4 @@ main reactor Minimal { printf("Hello World.\n"); =} -} \ No newline at end of file +} diff --git a/test/Cpp/src/StructAsState.lf b/test/Cpp/src/StructAsState.lf index 81aa4792fd..808a79e38c 100644 --- a/test/Cpp/src/StructAsState.lf +++ b/test/Cpp/src/StructAsState.lf @@ -1,9 +1,8 @@ // Check that a state variable can be a statically initialized struct target Cpp { - files: include/hello.h }; public preamble {= - #include "hello.h" + #include "include/hello.h" =} main reactor StructAsState { diff --git a/test/Cpp/src/StructAsType.lf b/test/Cpp/src/StructAsType.lf index fd8d9eee4d..f1aef0c7fd 100644 --- a/test/Cpp/src/StructAsType.lf +++ b/test/Cpp/src/StructAsType.lf @@ -1,12 +1,11 @@ // Source produces a statically allocated struct and sends a copy. target Cpp { - files: include/hello.h }; import Print from "StructPrint.lf"; public preamble {= - #include "hello.h" + #include "include/hello.h" =} reactor StaticSource { @@ -22,4 +21,4 @@ main reactor StructAsType { s = new StaticSource(); p = new Print(); s.out -> p.in; -} \ No newline at end of file +} diff --git a/test/Cpp/src/StructAsTypeDirect.lf b/test/Cpp/src/StructAsTypeDirect.lf index f3dde23c3b..44f811890f 100644 --- a/test/Cpp/src/StructAsTypeDirect.lf +++ b/test/Cpp/src/StructAsTypeDirect.lf @@ -1,12 +1,11 @@ // Source directly sends an implicitly dynamically created object target Cpp { - files: include/hello.h }; import Print from "StructPrint.lf"; public preamble {= - #include "hello.h" + #include "include/hello.h" =} reactor DirectSource { @@ -21,4 +20,4 @@ main reactor { s = new DirectSource(); p = new Print(); s.out -> p.in; -} \ No newline at end of file +} diff --git a/test/Cpp/src/StructParallel.lf b/test/Cpp/src/StructParallel.lf index 993e876273..5c8c0df9fe 100644 --- a/test/Cpp/src/StructParallel.lf +++ b/test/Cpp/src/StructParallel.lf @@ -3,13 +3,12 @@ // NOTE: Ideally, only one copy would be made, but this requires // is currently not supported by the C++ runtime. target Cpp { - files: include/hello.h }; import Scale from "StructScale.lf"; import Source, Print from "StructPrint.lf" public preamble {= - #include "hello.h" + #include "include/hello.h" =} main reactor { diff --git a/test/Cpp/src/StructPrint.lf b/test/Cpp/src/StructPrint.lf index b4f0c37d1f..525e475cad 100644 --- a/test/Cpp/src/StructPrint.lf +++ b/test/Cpp/src/StructPrint.lf @@ -1,9 +1,8 @@ // Source directly sends an implicitly dynamically created object target Cpp { - files: include/hello.h }; public preamble {= - #include "hello.h" + #include "include/hello.h" =} reactor Source { diff --git a/test/Cpp/src/StructScale.lf b/test/Cpp/src/StructScale.lf index 46f9ff8d6c..1e895f6737 100644 --- a/test/Cpp/src/StructScale.lf +++ b/test/Cpp/src/StructScale.lf @@ -3,13 +3,12 @@ // It modifies it and passes it to Print. It gets freed after // Print is done with it. target Cpp { - files: include/hello.h }; import Source, Print from "StructPrint.lf"; public preamble {= - #include "hello.h" + #include "include/hello.h" =} reactor Scale(scale:int(2)) {