Package Penpot 2.18.3 for Nix with NixOS module; afk devShell
Vendored from debling/nix-penpot (MIT). Fixes on top: - pnpm fetchers: drop --config.fetch-* flags (UND_ERR_INVALID_ARG on pnpm 11.27) and pin real store hashes (frontend x3, exporter, mcp) - deps-lock.json: add transitive funcool/beicon2 v2.0 so fake-git resolves potok2's pin during the shadow-cljs classpath build - expose frontend pnpm stores via passthru for rehashing - x86_64-linux only; devShell ships afk, jj, git
This commit is contained in:
commit
08b5496980
21 files changed
+7840
No files matched your search
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic repack of a jar, replicating stripJavaArchivesHook for the
|
||||
penpot backend uberjar.
|
||||
|
||||
Nix's stripJavaArchivesHook runs strip-nondeterminism, which drives the
|
||||
perl Archive::Zip module. Two problems made it unusable here:
|
||||
|
||||
* its per-member removeMember/addMember reorder loop is O(n^2) in the entry
|
||||
count (linear member search + full array rebuild per entry), so the
|
||||
54000-entry, 109 MB penpot jar spent ~32 minutes there;
|
||||
* Archive::Zip derives the DOS timestamps with localtime, so the result
|
||||
depended on the build machine's timezone.
|
||||
|
||||
This script reproduces the same semantics (jar.pm + zip.pm handlers) with the
|
||||
stdlib zipfile module: read each entry once, sort the entry list, write it in a
|
||||
single pass. The result is byte-identical across runs and timezones, and costs
|
||||
seconds instead of minutes.
|
||||
|
||||
Semantics replicated from strip-nondeterminism:
|
||||
* entry order: META-INF/ first, then META-INF/MANIFEST.MF, then lexicographic
|
||||
* canonical time: Archive::Zip SAFE_EPOCH (1980-01-01 12:01:00 local), used
|
||||
here as a fixed UTC tuple so the output never depends on $TZ
|
||||
* .clj entries are stamped 2 DOS seconds (1 unix second) earlier, so that
|
||||
Clojure never mistakes a .clj for a stale .class
|
||||
* META-INF/MANIFEST.MF: drop Bnd-LastModified:/Built-By: lines
|
||||
* .html with a `<!-- Generated by javadoc` marker in the first 1 KB: bump
|
||||
the generation date to the canonical date
|
||||
* nested .jar entries: recurse with the same semantics
|
||||
* extra fields 0x5455 (extended timestamps), 0x000a (NTFS), 0x7875 (ux
|
||||
uid/gid) are normalized; unknown fields are kept verbatim
|
||||
* archives with a META-INF/*.SF signature are left untouched
|
||||
* javac.sh and plexus compiler-arguments files are dropped
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
# Archive::Zip SAFE_EPOCH: 1980-01-01 12:01:00. The DOS timestamp below is the
|
||||
# same instant, but written as a fixed tuple: no localtime call on the way out.
|
||||
DOS_DATE = (1980, 1, 1)
|
||||
DOS_TIME = (12, 1, 0)
|
||||
DOS_EPOCH_SECONDS = 315576060
|
||||
|
||||
MANIFEST = "META-INF/MANIFEST.MF"
|
||||
MANIFEST_DIR = "META-INF/"
|
||||
JAVADOC_WINDOW = 1024
|
||||
JAVADOC_MARKER = b"<!-- Generated by javadoc"
|
||||
|
||||
# From jar.pm's extract helper: a `YYYY-MM-DD` following this class is the
|
||||
# javadoc generation timestamp.
|
||||
JAVADOC_DATE_RE = re.compile(rb"(<!-- Generated by javadoc[^>]*?\son\s)(\d{4}-\d{2}-\d{2})")
|
||||
|
||||
|
||||
def order_key(name: str):
|
||||
if name == MANIFEST_DIR:
|
||||
return (0, name)
|
||||
if name == MANIFEST:
|
||||
return (1, name)
|
||||
return (2, name)
|
||||
|
||||
|
||||
def dropped(name: str) -> bool:
|
||||
return name == "javac.sh" or re.fullmatch(
|
||||
r"org\.codehaus\.plexus\.compiler\.javac\.JavacCompiler\d+arguments", name
|
||||
) is not None
|
||||
|
||||
|
||||
def canonical_tuple(entry_name: str):
|
||||
if entry_name.endswith(".clj"):
|
||||
# 2 DOS seconds earlier, i.e. one unix second (DOS has 2 s granularity).
|
||||
seconds = DOS_TIME[0] * 3600 + DOS_TIME[1] * 60 + DOS_TIME[2] - 2
|
||||
return DOS_DATE + (seconds // 3600, (seconds % 3600) // 60, seconds % 60)
|
||||
return DOS_DATE + DOS_TIME
|
||||
|
||||
|
||||
def normalize_manifest(data: bytes) -> bytes:
|
||||
out = []
|
||||
for line in data.splitlines(keepends=True):
|
||||
if line.startswith(b"Bnd-LastModified:") or line.startswith(b"Built-By:"):
|
||||
continue
|
||||
out.append(line)
|
||||
return b"".join(out)
|
||||
|
||||
|
||||
def normalize_javadoc(data: bytes) -> bytes:
|
||||
head = data[:JAVADOC_WINDOW]
|
||||
if JAVADOC_MARKER not in head:
|
||||
return data
|
||||
|
||||
def repl(match):
|
||||
return match.group(1) + b"1980-01-01"
|
||||
|
||||
return JAVADOC_DATE_RE.sub(repl, data, count=1)
|
||||
|
||||
|
||||
def normalize_extra(extra: bytes, filename: str) -> bytes:
|
||||
"""Normalize known timestamp/identity extra fields, keep the rest as-is."""
|
||||
want = canonical_tuple(filename)
|
||||
out = bytearray()
|
||||
pos = 0
|
||||
while pos + 4 <= len(extra):
|
||||
field_id, field_len = struct.unpack_from("<HH", extra, pos)
|
||||
data = extra[pos + 4 : pos + 4 + field_len]
|
||||
if field_id == 0x5455: # extended timestamp
|
||||
if len(data) >= 5:
|
||||
data = data[:1] + b"\x00\x00\x00\x00" + data[5:]
|
||||
elif field_id == 0x000a: # NTFS timestamp
|
||||
if len(data) >= 4:
|
||||
reserved = data[:4]
|
||||
tags = bytearray()
|
||||
tag_pos = 4
|
||||
while tag_pos + 4 <= len(data):
|
||||
tag, tag_size = struct.unpack_from("<HH", data, tag_pos)
|
||||
tags += data[tag_pos : tag_pos + 4]
|
||||
if tag == 0x0001 and tag_size == 24:
|
||||
# 32 bytes of 100ns ticks since 1601, i.e. 12 bytes of
|
||||
# zeros followed by the unix seconds in the low word.
|
||||
ticks = b"\x00" * 12 + struct.pack("<I", int(DOS_EPOCH_SECONDS))
|
||||
tags += ticks
|
||||
else:
|
||||
tags += data[tag_pos + 4 : tag_pos + 4 + tag_size]
|
||||
tag_pos += 4 + tag_size
|
||||
data = reserved + bytes(tags)
|
||||
elif field_id == 0x7875: # Info-ZIP new unix extra field
|
||||
if len(data) >= 3 and data[0] == 1:
|
||||
uid_len, gid_len = data[1], data[2 + data[1]]
|
||||
data = (
|
||||
b"\x01"
|
||||
+ bytes([uid_len])
|
||||
+ b"\x00" * uid_len
|
||||
+ bytes([gid_len])
|
||||
+ b"\x00" * gid_len
|
||||
)
|
||||
out += struct.pack("<HH", field_id, len(data)) + data
|
||||
pos += 4 + field_len
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def normalize_member(name: str, data: bytes) -> bytes:
|
||||
if name == MANIFEST:
|
||||
return normalize_manifest(data)
|
||||
if name.endswith(".html"):
|
||||
return normalize_javadoc(data)
|
||||
if name.endswith(".jar"):
|
||||
return normalize_jar_bytes(data)
|
||||
return data
|
||||
|
||||
|
||||
def _rewrite(source, target):
|
||||
zin = zipfile.ZipFile(source)
|
||||
if any(n.startswith("META-INF/") and n.endswith(".SF") for n in zin.namelist()):
|
||||
# Signed jar: normalization would break the signature.
|
||||
return False, 0
|
||||
|
||||
with zipfile.ZipFile(target, "w") as zout:
|
||||
written = 0
|
||||
for item in sorted(zin.infolist(), key=lambda i: order_key(i.filename)):
|
||||
if dropped(item.filename):
|
||||
continue
|
||||
data = normalize_member(item.filename, zin.read(item.filename))
|
||||
date_time = canonical_tuple(item.filename)
|
||||
info = zipfile.ZipInfo(item.filename, date_time=date_time)
|
||||
info.compress_type = item.compress_type
|
||||
info.external_attr = item.external_attr
|
||||
info.internal_attr = item.internal_attr
|
||||
info.create_system = 3
|
||||
info.extra = normalize_extra(item.extra, item.filename)
|
||||
zout.writestr(info, data)
|
||||
written += 1
|
||||
return True, written
|
||||
|
||||
|
||||
def normalize_jar_bytes(data: bytes) -> bytes:
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as zin:
|
||||
if any(n.startswith("META-INF/") and n.endswith(".SF") for n in zin.namelist()):
|
||||
return data
|
||||
with zipfile.ZipFile(buffer, "w") as zout:
|
||||
for item in sorted(zin.infolist(), key=lambda i: order_key(i.filename)):
|
||||
if dropped(item.filename):
|
||||
continue
|
||||
info = zipfile.ZipInfo(item.filename, date_time=canonical_tuple(item.filename))
|
||||
info.compress_type = item.compress_type
|
||||
info.external_attr = item.external_attr
|
||||
info.internal_attr = item.internal_attr
|
||||
info.create_system = 3
|
||||
info.extra = normalize_extra(item.extra, item.filename)
|
||||
zout.writestr(info, normalize_member(item.filename, zin.read(item.filename)))
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2:
|
||||
print(f"usage: {sys.argv[0]} <jar>", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
path = sys.argv[1]
|
||||
tmp_fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(path) or ".", suffix=".jar")
|
||||
os.close(tmp_fd)
|
||||
try:
|
||||
handled, entries = _rewrite(path, tmp_path)
|
||||
if not handled:
|
||||
print(f"jar-strip: {path}: signed jar, leaving untouched", file=sys.stderr)
|
||||
return 0
|
||||
if entries == 0:
|
||||
print(f"jar-strip: {path}: no entries written", file=sys.stderr)
|
||||
return 1
|
||||
# Self-validation: reopen and verify every CRC before trusting the output.
|
||||
with zipfile.ZipFile(tmp_path) as check:
|
||||
bad = check.testzip()
|
||||
if bad is not None:
|
||||
print(f"jar-strip: {path}: CRC mismatch at {bad}", file=sys.stderr)
|
||||
return 1
|
||||
os.replace(tmp_path, path)
|
||||
print(f"jar-strip: {path}: normalized {entries} entries")
|
||||
return 0
|
||||
finally:
|
||||
if os.path.exists(tmp_path):
|
||||
os.unlink(tmp_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user