From 3fc77c2e8c9bb5a7775dbe695de75469291f51f3 Mon Sep 17 00:00:00 2001 From: Nolij Date: Fri, 29 Sep 2023 18:33:17 -0400 Subject: [PATCH] add generateAccessWidenerFromTransformer task --- felt-spindle/gradle.properties | 2 +- .../net/feltmc/spindle/SpindleExtension.java | 21 ++ .../net/feltmc/spindle/SpindlePlugin.java | 25 +- .../net/feltmc/spindle/mapping/Mappings.java | 129 +++++++++++ .../mapping/MergingMappingVisitor.java | 217 ++++++++++++++++++ .../ClassOverlayProcessor.java | 2 +- ...erateAccessWidenerFromTransformerTask.java | 215 +++++++++++++++++ 7 files changed, 607 insertions(+), 4 deletions(-) create mode 100644 felt-spindle/src/main/java/net/feltmc/spindle/SpindleExtension.java create mode 100644 felt-spindle/src/main/java/net/feltmc/spindle/mapping/Mappings.java create mode 100644 felt-spindle/src/main/java/net/feltmc/spindle/mapping/MergingMappingVisitor.java rename felt-spindle/src/main/java/net/feltmc/spindle/{ => processors}/ClassOverlayProcessor.java (99%) create mode 100644 felt-spindle/src/main/java/net/feltmc/spindle/task/GenerateAccessWidenerFromTransformerTask.java diff --git a/felt-spindle/gradle.properties b/felt-spindle/gradle.properties index 251ccc5..e57d24e 100644 --- a/felt-spindle/gradle.properties +++ b/felt-spindle/gradle.properties @@ -2,4 +2,4 @@ loom_version=1.3.8 asm_version=9.5 mapping_io_version=0.4.2 -base_version=0.1 \ No newline at end of file +base_version=0.2 \ No newline at end of file diff --git a/felt-spindle/src/main/java/net/feltmc/spindle/SpindleExtension.java b/felt-spindle/src/main/java/net/feltmc/spindle/SpindleExtension.java new file mode 100644 index 0000000..9cb09c1 --- /dev/null +++ b/felt-spindle/src/main/java/net/feltmc/spindle/SpindleExtension.java @@ -0,0 +1,21 @@ +package net.feltmc.spindle; + +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Optional; + +public abstract class SpindleExtension { + + @Optional + public abstract RegularFileProperty getAccessTransformerPath(); + + @Optional + public abstract Property getOverwriteAccessWidener(); + +// @Optional +// public abstract Property getAutoConvertATToAW(); +// +// @Optional +// public abstract Property getApplyATInDev(); + +} diff --git a/felt-spindle/src/main/java/net/feltmc/spindle/SpindlePlugin.java b/felt-spindle/src/main/java/net/feltmc/spindle/SpindlePlugin.java index c3d7c98..f1ee9bb 100644 --- a/felt-spindle/src/main/java/net/feltmc/spindle/SpindlePlugin.java +++ b/felt-spindle/src/main/java/net/feltmc/spindle/SpindlePlugin.java @@ -1,12 +1,33 @@ package net.feltmc.spindle; +import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.LoomGradleExtensionAPI; +import net.feltmc.spindle.processors.ClassOverlayProcessor; +import net.feltmc.spindle.task.GenerateAccessWidenerFromTransformerTask; import org.gradle.api.Plugin; import org.gradle.api.Project; public class SpindlePlugin implements Plugin { + public void apply(Project project) { - LoomGradleExtensionAPI loomExtension = project.getExtensions().findByType(LoomGradleExtensionAPI.class); - loomExtension.addMinecraftJarProcessor(ClassOverlayProcessor.class, "felt-spindle:overlays"); + final LoomGradleExtensionAPI loom = project.getExtensions().findByType(LoomGradleExtensionAPI.class); + if (loom == null) + throw new AssertionError("Fabric Loom not found!"); + + loom.addMinecraftJarProcessor(ClassOverlayProcessor.class, "felt-spindle:overlays"); + + var config = project.getExtensions().create("spindle", SpindleExtension.class); + + project.getTasks().register("generateAccessWidenerFromTransformer", GenerateAccessWidenerFromTransformerTask.class, task -> { + task.getAccessWidenerPath().set(loom.getAccessWidenerPath()); + task.getAccessTransformerPath().set(config.getAccessTransformerPath()); + task.getProjectMappingsFile().set(loom::getMappingsFile); + //noinspection UnstableApiUsage + task.getMinecraftVersion().set(loom.getIntermediateMappingsProvider().getMinecraftVersion()); + //noinspection UnstableApiUsage + task.getMinecraftVersionMeta().set(((LoomGradleExtension) loom).getMinecraftProvider().getVersionInfo()); + task.getOverwriteAccessWidener().set(config.getOverwriteAccessWidener()); + }); } + } diff --git a/felt-spindle/src/main/java/net/feltmc/spindle/mapping/Mappings.java b/felt-spindle/src/main/java/net/feltmc/spindle/mapping/Mappings.java new file mode 100644 index 0000000..153b372 --- /dev/null +++ b/felt-spindle/src/main/java/net/feltmc/spindle/mapping/Mappings.java @@ -0,0 +1,129 @@ +package net.feltmc.spindle.mapping; + +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta; +import net.fabricmc.mappingio.MappingVisitor; +import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; +import net.fabricmc.mappingio.format.ProGuardReader; +import net.fabricmc.mappingio.format.Tiny2Reader; +import net.fabricmc.mappingio.format.TsrgReader; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MemoryMappingTree; +import net.feltmc.spindle.util.LazyMap; + +import java.io.*; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Mappings { + + private static final String SRG_URL_TEMPLATE = "https://raw.githubusercontent.com/MinecraftForge/MCPConfig/master/versions/release/%s/joined.tsrg"; + private static final String MOJMAP_CLIENT_MAPPINGS = "client_mappings"; + private static final String MOJMAP_SERVER_MAPPINGS = "server_mappings"; + + public enum Namespace { + OBF("obf"), + SRG("srg"), + MOJMAP("mojmap"), + INTERMEDIARY("intermediary"), + NAMED("named"), + MERGED("merged"), + ; + + public final String name; + + Namespace(String name) { + this.name = name; + } + } + + public final MappingTree tree; + public final Map> map; + + public Mappings(final File projectMappingsFile, final String mcVersion, final MinecraftVersionMeta mcVersionMeta) throws IOException { + final MemoryMappingTree projectMappingsTree = new MemoryMappingTree(); + projectMappingsTree.visitHeader(); + Tiny2Reader.read(new FileReader(projectMappingsFile), projectMappingsTree); + projectMappingsTree.visitEnd(); + + final String srgURL = String.format(SRG_URL_TEMPLATE, mcVersion); + final MemoryMappingTree srgMappingsTree = new MemoryMappingTree(); + srgMappingsTree.visitHeader(); + //noinspection deprecation + TsrgReader.read(new InputStreamReader(new URL(srgURL).openStream()), srgMappingsTree); + srgMappingsTree.visitEnd(); + + final String mojMapClientUrl = mcVersionMeta.download(MOJMAP_CLIENT_MAPPINGS).url(); + final String mojMapServerUrl = mcVersionMeta.download(MOJMAP_SERVER_MAPPINGS).url(); + final MemoryMappingTree mojMapTree = new MemoryMappingTree(); + final MappingVisitor mojMapInverter = new MappingSourceNsSwitch(mojMapTree, Namespace.OBF.name); + mojMapTree.visitHeader(); + //noinspection deprecation + ProGuardReader.read(new InputStreamReader(new URL(mojMapClientUrl).openStream()), Namespace.MOJMAP.name, Namespace.OBF.name, mojMapInverter); + //noinspection deprecation + ProGuardReader.read(new InputStreamReader(new URL(mojMapServerUrl).openStream()), Namespace.MOJMAP.name, Namespace.OBF.name, mojMapInverter); + mojMapTree.visitEnd(); + + tree = MergingMappingVisitor.merge( + Namespace.OBF.name, List.of(projectMappingsTree, srgMappingsTree, mojMapTree), + Namespace.MERGED.name, Stream.of(Namespace.NAMED, Namespace.MOJMAP, Namespace.INTERMEDIARY).map(x -> x.name).toList());; + map = new LazyMap<>( + namespace -> + tree + .getClasses() + .stream() + .collect(Collectors.toMap(classMapping -> classMapping.getName(namespace.name), x -> x))); + } + + public String mapSignature(String signature, Namespace from, Namespace to) { + var builder = new StringBuilder(); + + for (int i = 0; i < signature.length(); i++) { + var c = signature.charAt(i); + builder.append(c); + + if (c == 'L') { + for (int j = ++i; j < signature.length(); j++) { + if (signature.charAt(j) == ';') { + var fromType = signature.substring(i, j); + var mapping = map.get(from).get(fromType); + if (mapping != null) + builder.append(mapping.getName(to.name)); + else + builder.append(fromType); + + i = j - 1; + break; + } + } + } + } + + return builder.toString(); + } + + public MappingTree.ClassMapping findClass(String className, Namespace namespace) { + return map.get(namespace).getOrDefault(className, null); + } + + public MappingTree.FieldMapping findField(MappingTree.ClassMapping classMapping, String fieldName, String fieldDesc, Namespace namespace) { + return classMapping.getField(fieldName, fieldDesc, tree.getNamespaceId(namespace.name)); + } + + public MappingTree.FieldMapping findField(MappingTree.ClassMapping classMapping, String fieldName, Namespace namespace) { + return classMapping + .getFields() + .stream() + .filter(x -> + x.getName(namespace.name).equals(fieldName)) + .findFirst() + .orElse(null); + } + + public MappingTree.MethodMapping findMethod(MappingTree.ClassMapping classMapping, String methodName, String methodDesc, Namespace namespace) { + return classMapping.getMethod(methodName, methodDesc, tree.getNamespaceId(namespace.name)); + } + +} diff --git a/felt-spindle/src/main/java/net/feltmc/spindle/mapping/MergingMappingVisitor.java b/felt-spindle/src/main/java/net/feltmc/spindle/mapping/MergingMappingVisitor.java new file mode 100644 index 0000000..b45d8d7 --- /dev/null +++ b/felt-spindle/src/main/java/net/feltmc/spindle/mapping/MergingMappingVisitor.java @@ -0,0 +1,217 @@ +package net.feltmc.spindle.mapping; + +import net.fabricmc.mappingio.MappedElementKind; +import net.fabricmc.mappingio.MappingVisitor; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MemoryMappingTree; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class MergingMappingVisitor implements MappingVisitor { + + private static List getAllNamespaces(Collection sources) { + return sources + .stream() + .flatMap(source -> source.getDstNamespaces().stream()) + .collect(Collectors.toUnmodifiableSet()) + .stream() + .toList(); + } + + @NotNull + private static MemoryMappingTree getMemoryMappingTree(String srcNamespace, Collection sources, List dstNamespaces) throws IOException { + final MemoryMappingTree tree = new MemoryMappingTree(); + tree.visitHeader(); + tree.visitNamespaces(srcNamespace, dstNamespaces); + tree.visitContent(); + + for (var source : sources) { + final Map namespaceMap = source + .getDstNamespaces() + .stream() + .collect(Collectors.toMap(source::getNamespaceId, tree::getNamespaceId)); + + source.accept(new MergingMappingVisitor(tree, namespaceMap)); + } + + return tree; + } + + public static MemoryMappingTree merge(String srcNamespace, Collection sources) throws IOException { + final List dstNamespaces = getAllNamespaces(sources); + + final MemoryMappingTree tree = getMemoryMappingTree(srcNamespace, sources, dstNamespaces); + + tree.visitEnd(); + + return tree; + } + + public static MemoryMappingTree merge(String srcNamespace, List sources, + String mergedNamespace, List mergePriority) throws IOException { + final List dstNamespaces = + Stream.concat(getAllNamespaces(sources).stream(), Stream.of(mergedNamespace)) + .collect(Collectors.toSet()) + .stream().toList(); + + final MemoryMappingTree tree = getMemoryMappingTree(srcNamespace, sources, dstNamespaces); + + final int mergedNamespaceId = tree.getNamespaceId(mergedNamespace); + + tree.accept(new MappingVisitor() { + private @NotNull String tryGetName(MappingTree.ElementMapping mapping) { + String mergedName = null; + + for (final String namespace : mergePriority) { + mergedName = mapping.getName(namespace); + if (mergedName != null) + break; + } + + return mergedName != null ? mergedName : mapping.getSrcName(); + } + + @Override + public void visitNamespaces(String srcNamespace, List dstNamespaces) throws IOException {} + + private @Nullable MappingTree.ClassMapping visitingClass = null; + @Override + public boolean visitClass(String srcName) throws IOException { + visitingClass = tree.getClass(srcName); + final String mergedName = tryGetName(visitingClass); + + if (tree.visitClass(srcName)) { + tree.visitDstName(MappedElementKind.CLASS, mergedNamespaceId, mergedName); + + return true; + } + + return false; + } + + @Override + public boolean visitField(String srcName, String srcDesc) throws IOException { + assert visitingClass != null; + final String mergedName = tryGetName(visitingClass.getField(srcName, srcDesc)); + + if (tree.visitField(srcName, srcDesc)) { + tree.visitDstName(MappedElementKind.FIELD, mergedNamespaceId, mergedName); + + return true; + } + + return false; + } + + private @Nullable MappingTree.MethodMapping visitingMethod = null; + @Override + public boolean visitMethod(String srcName, String srcDesc) throws IOException { + assert visitingClass != null; + visitingMethod = visitingClass.getMethod(srcName, srcDesc); + final String mergedName = tryGetName(visitingMethod); + + if (tree.visitMethod(srcName, srcDesc)) { + tree.visitDstName(MappedElementKind.METHOD, mergedNamespaceId, mergedName); + + return true; + } + + return false; + } + + @Override + public boolean visitMethodArg(int argPosition, int lvIndex, String srcName) throws IOException { + assert visitingMethod != null; + final String mergedName = tryGetName(visitingMethod.getArg(argPosition, lvIndex, srcName)); + + //noinspection ConstantValue + if (tree.visitMethodArg(argPosition, lvIndex, srcName)) { + tree.visitDstName(MappedElementKind.METHOD_ARG, mergedNamespaceId, mergedName); + + return true; + } + + return false; + } + + @Override + public boolean visitMethodVar(int lvtRowIndex, int lvIndex, int startOpIdx, String srcName) throws IOException { + assert visitingMethod != null; + final String mergedName = tryGetName(visitingMethod.getVar(lvtRowIndex, lvIndex, startOpIdx, srcName)); + + //noinspection ConstantValue + if (tree.visitMethodVar(lvtRowIndex, lvIndex, startOpIdx, srcName)) { + tree.visitDstName(MappedElementKind.METHOD_VAR, mergedNamespaceId, mergedName); + + return true; + } + + return false; + } + + @Override + public void visitDstName(MappedElementKind targetKind, int namespace, String name) throws IOException {} + + @Override + public void visitComment(MappedElementKind targetKind, String comment) throws IOException {} + }); + + tree.visitEnd(); + + return tree; + } + + + private final MappingVisitor target; + + private final Map namespaceMap; + + private MergingMappingVisitor(MappingVisitor target, Map namespaceMap) { + this.target = target; + this.namespaceMap = namespaceMap; + } + + @Override + public void visitNamespaces(String srcNamespace, List dstNamespaces) throws IOException {} + + @Override + public boolean visitClass(String srcName) throws IOException { + return target.visitClass(srcName); + } + + @Override + public boolean visitField(String srcName, String srcDesc) throws IOException { + return target.visitField(srcName, srcDesc); + } + + @Override + public boolean visitMethod(String srcName, String srcDesc) throws IOException { + return target.visitMethod(srcName, srcDesc); + } + + @Override + public boolean visitMethodArg(int argPosition, int lvIndex, String srcName) throws IOException { + return target.visitMethodArg(argPosition, lvIndex, srcName); + } + + @Override + public boolean visitMethodVar(int lvtRowIndex, int lvIndex, int startOpIdx, String srcName) throws IOException { + return target.visitMethodVar(lvtRowIndex, lvIndex, startOpIdx, srcName); + } + + @Override + public void visitDstName(MappedElementKind targetKind, int namespace, String name) throws IOException { + target.visitDstName(targetKind, namespaceMap.get(namespace), name); + } + + @Override + public void visitComment(MappedElementKind targetKind, String comment) throws IOException {} + +} diff --git a/felt-spindle/src/main/java/net/feltmc/spindle/ClassOverlayProcessor.java b/felt-spindle/src/main/java/net/feltmc/spindle/processors/ClassOverlayProcessor.java similarity index 99% rename from felt-spindle/src/main/java/net/feltmc/spindle/ClassOverlayProcessor.java rename to felt-spindle/src/main/java/net/feltmc/spindle/processors/ClassOverlayProcessor.java index f77c0cc..160befe 100644 --- a/felt-spindle/src/main/java/net/feltmc/spindle/ClassOverlayProcessor.java +++ b/felt-spindle/src/main/java/net/feltmc/spindle/processors/ClassOverlayProcessor.java @@ -22,7 +22,7 @@ * SOFTWARE. */ -package net.feltmc.spindle; +package net.feltmc.spindle.processors; import java.io.IOException; import java.nio.file.Path; diff --git a/felt-spindle/src/main/java/net/feltmc/spindle/task/GenerateAccessWidenerFromTransformerTask.java b/felt-spindle/src/main/java/net/feltmc/spindle/task/GenerateAccessWidenerFromTransformerTask.java new file mode 100644 index 0000000..2885440 --- /dev/null +++ b/felt-spindle/src/main/java/net/feltmc/spindle/task/GenerateAccessWidenerFromTransformerTask.java @@ -0,0 +1,215 @@ +package net.feltmc.spindle.task; + +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta; +import net.fabricmc.mappingio.tree.MappingTree; +import net.feltmc.spindle.mapping.Mappings; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; + +import java.io.*; +import java.nio.file.Files; + +public abstract class GenerateAccessWidenerFromTransformerTask extends DefaultTask { + + @InputFile + public abstract RegularFileProperty getProjectMappingsFile(); + + @Input + public abstract Property getMinecraftVersion(); + + @Input + public abstract Property getMinecraftVersionMeta(); + + @InputFile + @Optional + public abstract RegularFileProperty getAccessWidenerPath(); + + @InputFile + @Optional + public abstract RegularFileProperty getAccessTransformerPath(); + + @Input + @Optional + public abstract Property getOverwriteAccessWidener(); + + @TaskAction + public void generateAccessWidenerFromTransformer() throws IOException { + if (!getAccessTransformerPath().isPresent()) + throw new AssertionError("accessTransformerPath not set in build.gradle!"); + else if (!getAccessWidenerPath().isPresent()) + throw new AssertionError("accessWidenerPath not set in build.gradle!"); + + final boolean overwriteWidener = getOverwriteAccessWidener().getOrElse(false); + + final Mappings mappings = new Mappings(getProjectMappingsFile().get().getAsFile(), getMinecraftVersion().get(), getMinecraftVersionMeta().get()); + + final File widenerFile = getAccessWidenerPath().get().getAsFile(); + final File transformerFile = getAccessTransformerPath().get().getAsFile(); + + final File tempFile = Files.createTempFile("spindle", ".accesswidener").toFile(); + tempFile.deleteOnExit(); + + final BufferedReader widenerReader; + final BufferedReader transformerReader = new BufferedReader(new FileReader(transformerFile)); + final BufferedWriter tempWriter = new BufferedWriter(new FileWriter(tempFile)); + + String line; + + if (overwriteWidener) { + widenerReader = null; + + tempWriter.write("accessWidener v2 named"); + tempWriter.newLine(); + tempWriter.newLine(); + tempWriter.write("# spindle {"); + tempWriter.newLine(); + } else { + widenerReader = new BufferedReader(new FileReader(widenerFile)); + + while ((line = widenerReader.readLine()) != null) { + tempWriter.write(line); + tempWriter.newLine(); + if (line.matches("^\\s*#\\s*spindle\\s*\\{\\s*$")) + break; + } + if (line == null) + throw new AssertionError("No \"# spindle {\" block found!"); + } + + while ((line = transformerReader.readLine()) != null) { + if (line.startsWith("#")) { // keep AT comments + tempWriter.write(line); + tempWriter.newLine(); + + continue; + } else if (line.isBlank()) { + continue; + } + + tempWriter.write("# "); // insert AT line for reference and debugging + tempWriter.write(line); + tempWriter.newLine(); + + // TODO: rewrite below logic to take existing state + // in to account (right now it's potentially wasteful) + + final String content; + final int endParseIndex = line.indexOf('#'); + if (endParseIndex == -1) + content = line.strip(); + else + content = line.substring(0, endParseIndex).strip(); + + final String[] tokens = content.split("\\s+"); + + final int finalModIndex = tokens[0].length() - 2; + final String visibility; + final boolean unfinal; + if ((unfinal = tokens[0].endsWith("-f")) || tokens[0].endsWith("+f")) { + visibility = tokens[0].substring(0, finalModIndex); + } else { + visibility = tokens[0]; + } + + final String className = tokens[1].replaceAll("\\.", "/"); + + if (tokens.length == 2) { // target is a class; cease parsing + tempWriter.write("transitive-"); + if (unfinal) + tempWriter.write("extendable "); + else if (!visibility.equals("private")) + tempWriter.write("accessible "); + + tempWriter.write("class "); + tempWriter.write(className); + tempWriter.newLine(); + + continue; + } + + final MappingTree.ClassMapping classMapping = mappings.findClass(className, Mappings.Namespace.MERGED); + + final int methodDescIndex = tokens[2].indexOf('('); + if (methodDescIndex == -1) { // field + final MappingTree.FieldMapping fieldMapping = mappings.findField(classMapping, tokens[2], Mappings.Namespace.SRG); + + final String mappedName = fieldMapping.getName(Mappings.Namespace.MERGED.name); + final String mappedDesc = fieldMapping.getDesc(Mappings.Namespace.MERGED.name); + + final String suffix = " field %s %s %s".formatted(className, mappedName, mappedDesc); + + if (unfinal) { + tempWriter.write("transitive-mutable"); + tempWriter.write(suffix); + tempWriter.newLine(); + } + if (!visibility.equals("private")) { + tempWriter.write("transitive-accessible"); + tempWriter.write(suffix); + tempWriter.newLine(); + } + } else { // method + final String methodName = tokens[2].substring(0, methodDescIndex); + final String mappedDesc = tokens[2].substring(methodDescIndex); + final String methodDesc = mappings.mapSignature(mappedDesc, Mappings.Namespace.MERGED, Mappings.Namespace.SRG); + final MappingTree.MethodMapping methodMapping = mappings.findMethod(classMapping, methodName, methodDesc, Mappings.Namespace.SRG); + + final String mappedName = methodMapping.getName(Mappings.Namespace.MERGED.name); +// final String mappedDesc = methodMapping.getDesc(Mappings.Namespace.MERGED.name); + + final String suffix = " method %s %s %s".formatted(className, mappedName, mappedDesc); + + if (unfinal) { + tempWriter.write("transitive-extendable"); + tempWriter.write(suffix); + tempWriter.newLine(); + } + if (visibility.equals("public") || (!unfinal && !visibility.equals("private"))) { + tempWriter.write("transitive-accessible"); + tempWriter.write(suffix); + tempWriter.newLine(); + } + } + } + + if (widenerReader != null) { + var foundEnd = false; + + while ((line = widenerReader.readLine()) != null) { + if (line.matches("^\\s*#\\s*}")) { + foundEnd = true; + do { + tempWriter.write(line); + tempWriter.newLine(); + } while ((line = widenerReader.readLine()) != null); + break; + } + } + + widenerReader.close(); + + if (!foundEnd) + throw new AssertionError("No \"# }\" block found!"); + } else { + tempWriter.write("# }"); + tempWriter.newLine(); + } + + tempWriter.flush(); + tempWriter.close(); + transformerReader.close(); + + final BufferedReader tempReader = new BufferedReader(new FileReader(tempFile)); + final BufferedWriter widenerWriter = new BufferedWriter(new FileWriter(widenerFile)); + tempReader.transferTo(widenerWriter); + widenerWriter.flush(); + tempReader.close(); + widenerWriter.close(); + } + +}