From 89191fbef6e096b6abf580bbb5f94ccbb74ac59a Mon Sep 17 00:00:00 2001 From: Eugene Zhuravlev Date: Wed, 23 Apr 2025 23:06:14 +0200 Subject: [PATCH] zip output builder GitOrigin-RevId: fff1d7a477652f268392774943026d40d5bd9db4 --- .../jetbrains/jps/bazel/BazelIncBuilder.java | 4 + .../jetbrains/jps/bazel/ZipOutputBuilder.java | 4 +- .../jps/bazel/impl/ZipOutputBuilderImpl.java | 148 ++++++++++++++++-- 3 files changed, 138 insertions(+), 18 deletions(-) diff --git a/jps/jps-builders/src/org/jetbrains/jps/bazel/BazelIncBuilder.java b/jps/jps-builders/src/org/jetbrains/jps/bazel/BazelIncBuilder.java index 280eae5c86f6..c4f56d8e190b 100644 --- a/jps/jps-builders/src/org/jetbrains/jps/bazel/BazelIncBuilder.java +++ b/jps/jps-builders/src/org/jetbrains/jps/bazel/BazelIncBuilder.java @@ -142,6 +142,10 @@ public class BazelIncBuilder { return ExitCode.OK; } + catch (Throwable e) { + diagnostic.report(Message.create(null, e)); + return ExitCode.ERROR; + } finally { if (diagnostic instanceof PostponedDiagnosticSink) { // report postponed errors, if necessary diff --git a/jps/jps-builders/src/org/jetbrains/jps/bazel/ZipOutputBuilder.java b/jps/jps-builders/src/org/jetbrains/jps/bazel/ZipOutputBuilder.java index a4bfd3e0e7bd..bb16b4f08064 100644 --- a/jps/jps-builders/src/org/jetbrains/jps/bazel/ZipOutputBuilder.java +++ b/jps/jps-builders/src/org/jetbrains/jps/bazel/ZipOutputBuilder.java @@ -1,7 +1,7 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.jps.bazel; -import java.io.DataOutput; +import java.io.IOException; public interface ZipOutputBuilder { @@ -15,5 +15,5 @@ public interface ZipOutputBuilder { void deleteEntry(String entryName); - void write(DataOutput out); + void close(boolean saveChanges) throws IOException; } diff --git a/jps/jps-builders/src/org/jetbrains/jps/bazel/impl/ZipOutputBuilderImpl.java b/jps/jps-builders/src/org/jetbrains/jps/bazel/impl/ZipOutputBuilderImpl.java index f347e8c20de2..3d3d56adaafb 100644 --- a/jps/jps-builders/src/org/jetbrains/jps/bazel/impl/ZipOutputBuilderImpl.java +++ b/jps/jps-builders/src/org/jetbrains/jps/bazel/impl/ZipOutputBuilderImpl.java @@ -1,25 +1,45 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.jps.bazel.impl; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.jps.bazel.ZipOutputBuilder; -import java.io.DataOutput; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; -import java.util.TreeMap; +import java.nio.file.StandardCopyOption; +import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import static org.jetbrains.jps.javac.Iterators.*; public class ZipOutputBuilderImpl implements ZipOutputBuilder { private static final byte[] EMPTY_BYTES = new byte[0]; private final Map myEntries = new TreeMap<>(); + private final Map myDirectoryEntries = new HashMap<>(); + private final ZipFile myZipFile; + @NotNull + private final Path myOutputZip; + private boolean myHasChanges; - public ZipOutputBuilderImpl(Path outputZip) { - // todo: init from the previous zip + public ZipOutputBuilderImpl(Path outputZip) throws IOException { + myZipFile = new ZipFile(outputZip.toFile()); + myOutputZip = outputZip; + Enumeration entries = myZipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + myDirectoryEntries.put(entry.getName(), entry); + } + else { + myEntries.put(entry.getName(), EntryData.create(myZipFile, entry)); + } + } } @Override @@ -35,7 +55,7 @@ public class ZipOutputBuilderImpl implements ZipOutputBuilder { @Override public byte[] getContent(String entryName) { try { - return myEntries.getOrDefault(entryName, EntryData.EMPTY).getContent(); + return myEntries.getOrDefault(entryName, EntryData.DIR_DATA).getContent(); } catch (IOException e) { // todo: diagnostics @@ -45,20 +65,54 @@ public class ZipOutputBuilderImpl implements ZipOutputBuilder { @Override public void putEntry(String entryName, byte[] content) { - // todo: create intermediate directory entries - myEntries.put(entryName, EntryData.create(content)); + if (isDirectoryName(entryName)) { + throw new RuntimeException("Unexpected name with trailing slash for ZIP entry with content: \"" + entryName + "\""); + } + myEntries.put(entryName, EntryData.create(entryName, content)); + myHasChanges = true; } @Override public void deleteEntry(String entryName) { if (myEntries.remove(entryName) != null) { - // todo: update parent intermediate entry + myHasChanges = true; } } @Override - public void write(DataOutput out) { - // todo + public void close(boolean saveChanges) throws IOException { + if (!myHasChanges || !saveChanges) { + myZipFile.close(); + } + else { + // augment entry map with all currently present directory entries + for (String dirName : collect(flat(map(myEntries.keySet(), ZipOutputBuilderImpl::allParentNames)), new HashSet<>())) { + ZipEntry existingEntry = myDirectoryEntries.get(dirName); + myEntries.put(dirName, EntryData.create(myZipFile, existingEntry != null? existingEntry : new ZipEntry(dirName))); + } + Path newOutputName = getNewOutputName(); + try (var zos = new ZipOutputStream(Files.newOutputStream(newOutputName))) { + for (Iterator> it = myEntries.entrySet().iterator(); it.hasNext(); ) { + EntryData data = it.next().getValue(); + ZipEntry zipEntry = data.getZipEntry(); + zos.putNextEntry(zipEntry); + if (!zipEntry.isDirectory()) { + // either new content or the one loaded from the previous file + zos.write(data.getContent()); + } + it.remove(); + } + } + finally { + myZipFile.close(); + Files.move(newOutputName, myOutputZip, StandardCopyOption.REPLACE_EXISTING); + } + } + } + + private @NotNull Path getNewOutputName() { + // todo: handle situation when file exists + return myOutputZip.resolveSibling(myOutputZip.getFileName() + ".tmp"); } @Nullable @@ -72,28 +126,90 @@ public class ZipOutputBuilderImpl implements ZipOutputBuilder { } private interface EntryData { - EntryData EMPTY = () -> EMPTY_BYTES; + EntryData DIR_DATA = new EntryData() { + @Override + public byte[] getContent() { + return EMPTY_BYTES; + } + + @Override + public ZipEntry getZipEntry() { + return null; + } + }; byte[] getContent() throws IOException; - static EntryData create(byte[] content) { - return () -> content; + ZipEntry getZipEntry(); + + static EntryData create(String entryName, byte[] content) { + return new EntryData() { + private ZipEntry entry; + @Override + public byte[] getContent() { + return content; + } + + @Override + public ZipEntry getZipEntry() { + return entry != null? entry : (entry = new ZipEntry(entryName)); + } + }; } - static EntryData create(ZipFile zf, ZipEntry ze) { + static EntryData create(ZipFile zip, ZipEntry entry) { + if (entry.isDirectory()) { + return new EntryData() { + @Override + public byte[] getContent() { + return EMPTY_BYTES; + } + + @Override + public ZipEntry getZipEntry() { + return entry; + } + }; + } return new EntryData() { private byte[] loaded; @Override public byte[] getContent() throws IOException { if (loaded == null) { - try (InputStream is = zf.getInputStream(ze)) { + try (InputStream is = zip.getInputStream(entry)) { loaded = is.readAllBytes(); } } return loaded; } + + @Override + public ZipEntry getZipEntry() { + return entry; + } }; } } + private static Iterable allParentNames(String entryName) { + String parent = getParent(entryName); + return parent == null? List.of() : () -> new Iterator<>() { + private String next = parent; + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public String next() { + if (next == null) { + throw new NoSuchElementException(); + } + String result = next; + next = getParent(next); + return result; + } + }; + } + }