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:
kurogeek committed 2026-10-20 17:45:02 +00:00
commit 08b5496980
21 files changed
+7840

No files matched your search

+228
View File
@@ -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())