Skip to content

Commit b39aee7

Browse files
Allow serializing multiple modules individually or together (#1210)
* Minor clean-up to PureCompilerMojo * Minor fix to PureCompilerBinaryGenerator * Add a test for PureCompilerBinaryGenerator * Fix typo in FileDeserializer * Serialize multiple modules one at a time in PureCompilerBinaryGenerator * Allow serializing multiple modules one at a time in PureCompilerBinaryGenerator * Allow serializing multiple modules one at a time in PureCompilerMojo * Add more tests for PureCompilerBinaryGenerator
1 parent d577f0c commit b39aee7

File tree

4 files changed

+330
-50
lines changed

4 files changed

+330
-50
lines changed

legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/generator/compiler/PureCompilerBinaryGenerator.java

Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import org.eclipse.collections.api.factory.Lists;
1818
import org.eclipse.collections.api.factory.Sets;
19+
import org.eclipse.collections.api.list.ListIterable;
1920
import org.eclipse.collections.api.list.MutableList;
2021
import org.eclipse.collections.api.set.MutableSet;
2122
import org.eclipse.collections.api.set.SetIterable;
@@ -38,11 +39,17 @@
3839
import org.finos.legend.pure.m3.serialization.runtime.PureCompilerLoader;
3940
import org.finos.legend.pure.m3.serialization.runtime.PureRuntime;
4041
import org.finos.legend.pure.m3.serialization.runtime.PureRuntimeBuilder;
42+
import org.finos.legend.pure.m3.serialization.runtime.RepositoryComparator;
4143
import org.finos.legend.pure.m4.exception.PureCompilationException;
4244
import org.finos.legend.pure.m4.serialization.grammar.antlr.PureParserException;
4345
import org.slf4j.Logger;
4446
import org.slf4j.LoggerFactory;
4547

48+
import java.io.IOException;
49+
import java.io.UncheckedIOException;
50+
import java.net.MalformedURLException;
51+
import java.net.URL;
52+
import java.net.URLClassLoader;
4653
import java.nio.file.Path;
4754
import java.nio.file.Paths;
4855
import java.util.Set;
@@ -65,10 +72,20 @@ public static void main(String[] args)
6572

6673
public static void serializeModules(Path outputDirectory, Iterable<String> modules)
6774
{
68-
serializeModules(outputDirectory, null, modules, null);
75+
serializeModules(outputDirectory, modules, modules == null);
76+
}
77+
78+
public static void serializeModules(Path outputDirectory, Iterable<String> modules, boolean serializeIndividually)
79+
{
80+
serializeModules(outputDirectory, null, modules, null, serializeIndividually);
6981
}
7082

7183
public static void serializeModules(Path outputDirectory, ClassLoader classLoader, Iterable<String> modules, Iterable<String> excludedModules)
84+
{
85+
serializeModules(outputDirectory, classLoader, modules, excludedModules, modules == null);
86+
}
87+
88+
public static void serializeModules(Path outputDirectory, ClassLoader classLoader, Iterable<String> modules, Iterable<String> excludedModules, boolean serializeIndividually)
7289
{
7390
long start = System.nanoTime();
7491
SetIterable<String> moduleSet = (modules == null) ? Sets.immutable.empty() :
@@ -87,9 +104,7 @@ public static void serializeModules(Path outputDirectory, ClassLoader classLoade
87104
try
88105
{
89106
FilePathProvider filePathProvider = FilePathProvider.builder().withLoadedExtensions(currentClassLoader).build();
90-
RepositoryInfo repositoryInfo = resolveRepositories(moduleSet, excludedModules, currentClassLoader, filePathProvider);
91-
PureRuntime runtime = compile(currentClassLoader, repositoryInfo.toCompile);
92-
serialize(outputDirectory, repositoryInfo.toSerialize, runtime, filePathProvider);
107+
serializeModules(outputDirectory, currentClassLoader, moduleSet, excludedModules, filePathProvider, serializeIndividually);
93108
}
94109
catch (Throwable t)
95110
{
@@ -107,18 +122,22 @@ public static void serializeModules(Path outputDirectory, ClassLoader classLoade
107122
}
108123
}
109124

110-
private static RepositoryInfo resolveRepositories(SetIterable<String> modules, Iterable<String> excludedModules, ClassLoader classLoader, FilePathProvider filePathProvider)
125+
private static void serializeModules(Path outputDirectory, ClassLoader classLoader, SetIterable<String> modules, Iterable<String> excludedModules, FilePathProvider filePathProvider, boolean serializeIndividually)
111126
{
112-
MutableList<CodeRepository> foundRepos = CodeRepositoryProviderHelper.findCodeRepositories(true).toList();
127+
// Build the full repository set (with exclusions applied)
128+
MutableList<CodeRepository> foundRepos = CodeRepositoryProviderHelper.findCodeRepositories(classLoader, true).toList();
113129
LOGGER.debug("Found repositories: {}", foundRepos.asLazy().collect(CodeRepository::getName));
114-
CodeRepositorySet.Builder builder = CodeRepositorySet.builder().withCodeRepositories(foundRepos);
130+
CodeRepositorySet.Builder fullSetBuilder = CodeRepositorySet.builder().withCodeRepositories(foundRepos);
115131
if (excludedModules != null)
116132
{
117-
builder.withoutCodeRepositories(excludedModules);
133+
fullSetBuilder.withoutCodeRepositories(excludedModules);
118134
}
135+
136+
// Determine which modules to serialize
119137
SetIterable<String> toSerialize;
120138
if (modules.isEmpty())
121139
{
140+
// Serialize all modules that don't already have a manifest on the classpath
122141
SetIterable<String> excludedSet = (excludedModules == null) ? Sets.immutable.empty() : Sets.mutable.withAll(excludedModules);
123142
toSerialize = foundRepos.collectIf(
124143
repo -> !excludedSet.contains(repo.getName()) && classLoader.getResource(filePathProvider.getModuleManifestResourceName(repo.getName())) == null,
@@ -132,14 +151,73 @@ private static RepositoryInfo resolveRepositories(SetIterable<String> modules, I
132151
}
133152
if (toSerialize.notEmpty())
134153
{
135-
builder.subset(toSerialize);
154+
fullSetBuilder.subset(toSerialize);
155+
}
156+
CodeRepositorySet allRepositories = fullSetBuilder.build();
157+
158+
if (serializeIndividually)
159+
{
160+
// Order the modules to serialize by dependency (dependencies first)
161+
ListIterable<String> orderedModules;
162+
if (toSerialize.size() == 1)
163+
{
164+
orderedModules = Lists.immutable.with(toSerialize.getAny());
165+
}
166+
else
167+
{
168+
orderedModules = toSerialize.isEmpty() ?
169+
CodeRepository.toSortedRepositoryList(allRepositories.getRepositories()).collect(CodeRepository::getName) :
170+
toSerialize.toSortedList(new RepositoryComparator(allRepositories.getRepositories()));
171+
LOGGER.debug("Serializing modules in order: {}", orderedModules);
172+
}
173+
174+
// Create a class loader that includes the output directory so that previously serialized modules can be loaded
175+
try (URLClassLoader outputClassLoader = newClassLoaderWithOutputDirectory(classLoader, outputDirectory))
176+
{
177+
PureCompilerLoader loader = PureCompilerLoader.newLoader(outputClassLoader);
178+
orderedModules.forEach(m -> serializeModules(outputDirectory, classLoader, Sets.immutable.with(m), allRepositories, loader, filePathProvider));
179+
}
180+
catch (IOException e)
181+
{
182+
throw new UncheckedIOException("Error closing class loader", e);
183+
}
184+
}
185+
else
186+
{
187+
serializeModules(outputDirectory, classLoader, toSerialize, allRepositories, PureCompilerLoader.newLoader(classLoader), filePathProvider);
136188
}
137-
CodeRepositorySet resolvedRepositories = builder.build();
138-
LOGGER.debug("Resolved repositories: {}", resolvedRepositories.getRepositoryNames());
139-
return new RepositoryInfo(resolvedRepositories, toSerialize);
140189
}
141190

142-
private static PureRuntime compile(ClassLoader classLoader, CodeRepositorySet codeRepositories)
191+
private static void serializeModules(Path outputDirectory, ClassLoader classLoader, SetIterable<String> modulesToSerialize, CodeRepositorySet modulesToCompile, PureCompilerLoader loader, FilePathProvider filePathProvider)
192+
{
193+
long moduleStart = System.nanoTime();
194+
LOGGER.info("Starting compilation and serialization of {}", (modulesToSerialize.size() == 1) ? modulesToSerialize.getAny() : modulesToSerialize);
195+
try
196+
{
197+
LOGGER.debug("Compiling modules {}", modulesToCompile.getRepositoryNames());
198+
PureRuntime runtime = compile(classLoader, loader, modulesToCompile);
199+
serialize(outputDirectory, modulesToSerialize, runtime, filePathProvider);
200+
}
201+
finally
202+
{
203+
long moduleEnd = System.nanoTime();
204+
LOGGER.info("Finished compilation and serialization of {} in {}s", (modulesToSerialize.size() == 1) ? modulesToSerialize.getAny() : modulesToSerialize, (moduleEnd - moduleStart) / 1_000_000_000.0);
205+
}
206+
}
207+
208+
private static URLClassLoader newClassLoaderWithOutputDirectory(ClassLoader parent, Path outputDirectory)
209+
{
210+
try
211+
{
212+
return new URLClassLoader(new URL[]{outputDirectory.toUri().toURL()}, parent);
213+
}
214+
catch (MalformedURLException e)
215+
{
216+
throw new RuntimeException("Error creating class loader with output directory: " + outputDirectory, e);
217+
}
218+
}
219+
220+
private static PureRuntime compile(ClassLoader classLoader, PureCompilerLoader loader, CodeRepositorySet codeRepositories)
143221
{
144222
long start = System.nanoTime();
145223
LOGGER.info("Starting compilation");
@@ -150,7 +228,6 @@ private static PureRuntime compile(ClassLoader classLoader, CodeRepositorySet co
150228
.setTransactionalByDefault(false)
151229
.build();
152230

153-
PureCompilerLoader loader = PureCompilerLoader.newLoader(classLoader);
154231
MutableList<String> reposToLoad = Lists.mutable.empty();
155232
MutableList<String> reposToCompile = Lists.mutable.empty();
156233
codeRepositories.getRepositoryNames().forEach(r -> (loader.canLoad(r) ? reposToLoad : reposToCompile).add(r));
@@ -264,16 +341,4 @@ private static void serialize(Path outputDirectory, SetIterable<String> modules,
264341
LOGGER.info("Finished serialization in {}s", (end - start) / 1_000_000_000.0);
265342
}
266343
}
267-
268-
private static class RepositoryInfo
269-
{
270-
private final CodeRepositorySet toCompile;
271-
private final SetIterable<String> toSerialize;
272-
273-
private RepositoryInfo(CodeRepositorySet toCompile, SetIterable<String> toSerialize)
274-
{
275-
this.toCompile = toCompile;
276-
this.toSerialize = toSerialize;
277-
}
278-
}
279344
}

legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/serialization/compiler/file/FileDeserializer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1121,7 +1121,7 @@ public Builder withSerializers(ConcreteElementDeserializer elementDeserializer,
11211121
public FileDeserializer build()
11221122
{
11231123
Objects.requireNonNull(this.filePathProvider, "file path provider is required");
1124-
Objects.requireNonNull(this.elementDeserializer, "concrete element serializer is required");
1124+
Objects.requireNonNull(this.elementDeserializer, "concrete element deserializer is required");
11251125
Objects.requireNonNull(this.moduleSerializer, "module serializer is required");
11261126
return new FileDeserializer(this.filePathProvider, this.elementDeserializer, this.moduleSerializer);
11271127
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright 2026 Goldman Sachs
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package org.finos.legend.pure.m3.generator.compiler;
16+
17+
import org.eclipse.collections.api.factory.Lists;
18+
import org.eclipse.collections.api.factory.Sets;
19+
import org.eclipse.collections.api.list.MutableList;
20+
import org.eclipse.collections.api.set.MutableSet;
21+
import org.finos.legend.pure.m3.serialization.compiler.element.ConcreteElementDeserializer;
22+
import org.finos.legend.pure.m3.serialization.compiler.file.FileDeserializer;
23+
import org.finos.legend.pure.m3.serialization.compiler.file.FilePathProvider;
24+
import org.finos.legend.pure.m3.serialization.compiler.metadata.ModuleManifest;
25+
import org.finos.legend.pure.m3.serialization.compiler.metadata.ModuleMetadataSerializer;
26+
import org.junit.Assert;
27+
import org.junit.ClassRule;
28+
import org.junit.Test;
29+
import org.junit.rules.TemporaryFolder;
30+
31+
import java.io.IOException;
32+
import java.io.UncheckedIOException;
33+
import java.nio.file.Files;
34+
import java.nio.file.Path;
35+
import java.util.Arrays;
36+
import java.util.stream.Stream;
37+
38+
public class TestPureCompilerBinaryGenerator
39+
{
40+
@ClassRule
41+
public static TemporaryFolder TMP = new TemporaryFolder();
42+
43+
private static final String PLATFORM = "platform";
44+
private static final String TEST_REPO = "test_generic_repository";
45+
private static final String OTHER_TEST_REPO = "other_test_generic_repository";
46+
47+
@Test
48+
public void testSerializeSingleModule() throws IOException
49+
{
50+
Path outputDirectory = TMP.newFolder().toPath();
51+
PureCompilerBinaryGenerator.serializeModules(outputDirectory, Lists.immutable.with(TEST_REPO));
52+
53+
FileDeserializer deserializer = newFileDeserializer();
54+
assertModuleSerialized(deserializer, outputDirectory, TEST_REPO);
55+
assertModuleNotSerialized(deserializer, outputDirectory, PLATFORM);
56+
assertModuleNotSerialized(deserializer, outputDirectory, OTHER_TEST_REPO);
57+
}
58+
59+
@Test
60+
public void testSerializeAllModules() throws IOException
61+
{
62+
FileDeserializer deserializer = newFileDeserializer();
63+
64+
// Check the case where all modules are implicitly specified and serialized individually
65+
Path implicitOutputDir = TMP.newFolder().toPath();
66+
PureCompilerBinaryGenerator.serializeModules(implicitOutputDir, null, true);
67+
68+
assertModuleSerialized(deserializer, implicitOutputDir, PLATFORM);
69+
assertModuleSerialized(deserializer, implicitOutputDir, TEST_REPO);
70+
assertModuleSerialized(deserializer, implicitOutputDir, OTHER_TEST_REPO);
71+
72+
// Check the case where all modules are explicitly specified and serialized together
73+
Path explicitOutputDir = TMP.newFolder().toPath();
74+
PureCompilerBinaryGenerator.serializeModules(explicitOutputDir, Lists.immutable.with(PLATFORM, TEST_REPO, OTHER_TEST_REPO), false);
75+
76+
assertModuleSerialized(deserializer, explicitOutputDir, PLATFORM);
77+
assertModuleSerialized(deserializer, explicitOutputDir, TEST_REPO);
78+
assertModuleSerialized(deserializer, explicitOutputDir, OTHER_TEST_REPO);
79+
80+
// Assert that the directories have identical content
81+
assertDirectoriesEquivalent(implicitOutputDir, explicitOutputDir);
82+
}
83+
84+
@Test
85+
public void testSerializeMultipleButNotAllModules() throws IOException
86+
{
87+
Path outputDirectory = TMP.newFolder().toPath();
88+
PureCompilerBinaryGenerator.serializeModules(outputDirectory, Lists.immutable.with(TEST_REPO, OTHER_TEST_REPO));
89+
90+
FileDeserializer deserializer = newFileDeserializer();
91+
assertModuleSerialized(deserializer, outputDirectory, TEST_REPO);
92+
assertModuleSerialized(deserializer, outputDirectory, OTHER_TEST_REPO);
93+
assertModuleNotSerialized(deserializer, outputDirectory, PLATFORM);
94+
}
95+
96+
@Test
97+
public void testSerializeWithExcludedModule() throws IOException
98+
{
99+
Path outputDirectory = TMP.newFolder().toPath();
100+
PureCompilerBinaryGenerator.serializeModules(outputDirectory, null, null, Lists.immutable.with(OTHER_TEST_REPO));
101+
102+
FileDeserializer deserializer = newFileDeserializer();
103+
assertModuleSerialized(deserializer, outputDirectory, PLATFORM);
104+
assertModuleSerialized(deserializer, outputDirectory, TEST_REPO);
105+
assertModuleNotSerialized(deserializer, outputDirectory, OTHER_TEST_REPO);
106+
}
107+
108+
private static void assertModuleSerialized(FileDeserializer deserializer, Path outputDirectory, String moduleName)
109+
{
110+
Assert.assertTrue(moduleName + " manifest should exist", deserializer.moduleManifestExists(outputDirectory, moduleName));
111+
ModuleManifest manifest = deserializer.deserializeModuleManifest(outputDirectory, moduleName);
112+
Assert.assertEquals(moduleName, manifest.getModuleName());
113+
manifest.forEachElement(element ->
114+
{
115+
String elementPath = element.getPath();
116+
Assert.assertTrue(moduleName + " / " + elementPath, deserializer.elementExists(outputDirectory, elementPath));
117+
});
118+
}
119+
120+
private static void assertModuleNotSerialized(FileDeserializer deserializer, Path outputDirectory, String moduleName)
121+
{
122+
Assert.assertFalse(moduleName + " manifest should not exist", deserializer.moduleManifestExists(outputDirectory, moduleName));
123+
}
124+
125+
private static void assertDirectoriesEquivalent(Path dir1, Path dir2)
126+
{
127+
// First check that relative files paths are the same
128+
MutableSet<Path> paths1 = Sets.mutable.empty();
129+
MutableSet<Path> paths2 = Sets.mutable.empty();
130+
try (Stream<Path> stream1 = Files.walk(dir1);
131+
Stream<Path> stream2 = Files.walk(dir2))
132+
{
133+
stream1.map(dir1::relativize).forEach(paths1::add);
134+
stream2.map(dir2::relativize).forEach(paths2::add);
135+
}
136+
catch (IOException e)
137+
{
138+
throw new UncheckedIOException(e);
139+
}
140+
Assert.assertEquals(paths1, paths2);
141+
142+
MutableList<Path> mismatchFiles = Lists.mutable.empty();
143+
paths1.forEach(path ->
144+
{
145+
Path file1 = dir1.resolve(path);
146+
Path file2 = dir2.resolve(path);
147+
if (Files.isDirectory(file1))
148+
{
149+
if (!Files.isDirectory(file2))
150+
{
151+
mismatchFiles.add(path);
152+
}
153+
}
154+
else if (Files.isDirectory(file2))
155+
{
156+
mismatchFiles.add(path);
157+
}
158+
else
159+
{
160+
try
161+
{
162+
byte[] bytes1 = Files.readAllBytes(file1);
163+
byte[] bytes2 = Files.readAllBytes(file2);
164+
if (!Arrays.equals(bytes1, bytes2))
165+
{
166+
mismatchFiles.add(path);
167+
}
168+
}
169+
catch (IOException e)
170+
{
171+
throw new UncheckedIOException(e);
172+
}
173+
}
174+
});
175+
Assert.assertEquals(Lists.fixedSize.empty(), mismatchFiles);
176+
}
177+
178+
private static FileDeserializer newFileDeserializer()
179+
{
180+
return FileDeserializer.builder()
181+
.withFilePathProvider(FilePathProvider.builder().withLoadedExtensions().build())
182+
.withConcreteElementDeserializer(ConcreteElementDeserializer.builder().withLoadedExtensions().build())
183+
.withModuleMetadataSerializer(ModuleMetadataSerializer.builder().withLoadedExtensions().build())
184+
.build();
185+
}
186+
}

0 commit comments

Comments
 (0)