98 lines
4.7 KiB
Python
98 lines
4.7 KiB
Python
"""Build the Zenodo archive (open release, Apache-2.0): the repository at HEAD of release
|
|
(`git archive`), its full history (`git bundle`), the simbracket wheels, README, requirements, the
|
|
reproduction script, LICENSE, NOTICE and SHA256SUMS - zipped as sim-numerics-<version>.zip.
|
|
|
|
Refuses to build if the tree or the bundled history contains the server address or the
|
|
specifics of the next method, if HEAD is not release, or if a text file of the archive has
|
|
Windows line ends.
|
|
|
|
python -u zenodo/make_open_package.py 1.0.0
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import zipfile
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
VERSION = sys.argv[1] if len(sys.argv) > 1 else "1.0.0"
|
|
NAME = f"sim-numerics-{VERSION}"
|
|
OUT = os.path.join(ROOT, "zenodo_build")
|
|
PKG = os.path.join(OUT, NAME)
|
|
# The strings that must appear in no commit are read from a file OUTSIDE the repository (named by
|
|
# $ZENODO_FORBIDDEN): listing them here would put them into the history this check protects.
|
|
FORBIDDEN_FILE = os.environ.get("ZENODO_FORBIDDEN", "")
|
|
WHEELS = ["lib/dist/simbracket-1.0.0-cp310-abi3-linux_x86_64.whl",
|
|
"lib/dist/simbracket-1.0.0-cp310-abi3-win_amd64.whl"]
|
|
CRLF = bytes([13, 10])
|
|
|
|
|
|
def git(*args, **kw):
|
|
return subprocess.run(["git", "-C", ROOT, *args], check=True, capture_output=True, **kw)
|
|
|
|
|
|
def main():
|
|
branch = git("branch", "--show-current", text=True).stdout.strip()
|
|
assert branch == "release", branch
|
|
assert not git("status", "--porcelain", "--untracked-files=no", text=True).stdout.strip(), \
|
|
"commit everything first: the archive is HEAD"
|
|
assert FORBIDDEN_FILE and os.path.isfile(FORBIDDEN_FILE), "set ZENODO_FORBIDDEN to the private list"
|
|
assert not os.path.abspath(FORBIDDEN_FILE).startswith(ROOT), "the list must live outside the repository"
|
|
forbidden = [ln.strip() for ln in open(FORBIDDEN_FILE, encoding="utf8") if ln.strip()]
|
|
for s in forbidden: # every commit of the branch
|
|
hits = subprocess.run(["git", "-C", ROOT, "log", "release", "-p", "-S", s, "--format=%h"],
|
|
capture_output=True, text=True, encoding="utf8").stdout.split()
|
|
assert not hits, (s, hits[:3])
|
|
if os.path.isdir(OUT):
|
|
shutil.rmtree(OUT)
|
|
os.makedirs(os.path.join(PKG, "wheels"))
|
|
tar = os.path.join(OUT, "source.tar")
|
|
# core.autocrlf=false: the archive carries the repository's LF line ends, not Windows' CRLF
|
|
git("-c", "core.autocrlf=false", "archive", "--format=tar", "--prefix=source/", "-o", tar, "HEAD")
|
|
shutil.unpack_archive(tar, PKG)
|
|
os.remove(tar)
|
|
# HEAD too, so that `git clone sim-numerics.bundle` checks the branch out
|
|
git("bundle", "create", os.path.join(PKG, "sim-numerics.bundle"), "HEAD", "release")
|
|
for w in WHEELS:
|
|
shutil.copy(os.path.join(ROOT, w), os.path.join(PKG, "wheels"))
|
|
src = os.path.join(PKG, "source") # from the archive: LF, exactly as committed
|
|
for f in ("README.md", "requirements.txt", "reproduce.sh"):
|
|
shutil.copy(os.path.join(src, "zenodo", f), os.path.join(PKG, f))
|
|
for f in ("LICENSE", "NOTICE"):
|
|
shutil.copy(os.path.join(src, f), os.path.join(PKG, f))
|
|
bad = []
|
|
for d, _, files in os.walk(PKG):
|
|
for f in files:
|
|
if f.endswith((".py", ".sh", ".md", ".txt")) or f in ("LICENSE", "NOTICE"):
|
|
if CRLF in open(os.path.join(d, f), "rb").read():
|
|
bad.append(os.path.relpath(os.path.join(d, f), PKG))
|
|
assert not bad, ("Windows line ends", bad[:5])
|
|
sums = []
|
|
for d, _, files in os.walk(PKG):
|
|
for f in sorted(files):
|
|
p = os.path.join(d, f)
|
|
rel = os.path.relpath(p, PKG).replace("\\", "/")
|
|
sums.append(f"{hashlib.sha256(open(p, 'rb').read()).hexdigest()} {rel}")
|
|
with open(os.path.join(PKG, "SHA256SUMS"), "w", encoding="ascii", newline="\n") as fh:
|
|
fh.write("\n".join(sorted(sums, key=lambda s: s[66:])) + "\n")
|
|
zp = os.path.join(OUT, NAME + ".zip")
|
|
with zipfile.ZipFile(zp, "w", zipfile.ZIP_DEFLATED) as z:
|
|
for d, _, files in os.walk(PKG):
|
|
for f in files:
|
|
p = os.path.join(d, f)
|
|
info = zipfile.ZipInfo.from_file(p, os.path.relpath(p, OUT).replace("\\", "/"))
|
|
if f.endswith(".sh"):
|
|
info.external_attr = 0o755 << 16
|
|
with open(p, "rb") as src_fh:
|
|
z.writestr(info, src_fh.read(), zipfile.ZIP_DEFLATED)
|
|
head = git("rev-parse", "--short", "HEAD", text=True).stdout.strip()
|
|
print(f"{zp}: {os.path.getsize(zp) / 1e6:.1f} MB, {len(sums)} files, source at {head}")
|
|
print("zenodo package done")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|