export_source.py #1

  • //
  • guest/
  • russell_jackson/
  • sdp/
  • test/
  • containers/
  • export_source.py
  • View
  • Commits
  • Open Download .zip Download (3 KB)
#!/usr/bin/env python3
"""Export current maintained Perforce files, excluding pending/depot deletions."""
import argparse
import hashlib
import json
import marshal
import os
from pathlib import Path
import shutil
import subprocess
import sys


def perforce_files(source):
    command = ["p4", "-G", "fstat", "-T", "depotFile,clientFile,headAction,action,haveRev", str(source / "...")]
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    records, errors = [], []
    while True:
        try:
            record = marshal.load(process.stdout)
        except EOFError:
            break
        record = {(k.decode() if isinstance(k, bytes) else k): (v.decode() if isinstance(v, bytes) else v) for k, v in record.items()}
        if record.get("code") == "error":
            errors.append(record.get("data", str(record)))
        elif record.get("clientFile"):
            if record.get("action") in ("delete", "move/delete"):
                continue
            if record.get("headAction") in ("delete", "move/delete") and record.get("action") not in ("add", "move/add", "branch"):
                continue
            records.append(record)
    error_text = process.stderr.read().decode()
    if process.wait() or errors:
        raise RuntimeError("Perforce inventory failed: " + "\n".join(errors) + error_text)
    return records


def export(source, destination):
    source = source.resolve()
    if destination.exists():
        raise ValueError("Export destination must not exist")
    records = perforce_files(source)
    if not records:
        raise ValueError("No maintained Perforce files found")
    manifest = []
    # Validate all input files before creating the export.
    for record in records:
        path = Path(record["clientFile"])
        relative = path.relative_to(source)
        if not path.is_file() and not path.is_symlink():
            raise ValueError("Maintained file missing locally: " + str(relative))
        if path.is_symlink():
            data = os.readlink(path).encode()
        else:
            data = path.read_bytes()
        manifest.append({"path": relative.as_posix(), "depot": record.get("depotFile"), "haveRev": record.get("haveRev"), "action": record.get("action", ""), "sha256": hashlib.sha256(data).hexdigest(), "symlink": os.readlink(path) if path.is_symlink() else None})
    destination.mkdir(parents=True)
    for entry, record in zip(manifest, records):
        target = destination / entry["path"]
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(record["clientFile"], target, follow_symlinks=False)
        copied = os.readlink(target).encode() if target.is_symlink() else target.read_bytes()
        if hashlib.sha256(copied).hexdigest() != entry["sha256"]:
            raise RuntimeError("Source changed during export: " + entry["path"])
    (destination / ".export-manifest.json").write_text(json.dumps(sorted(manifest, key=lambda row: row["path"]), indent=2) + "\n")
    return manifest


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("source", type=Path)
    parser.add_argument("destination", type=Path)
    args = parser.parse_args()
    try:
        entries = export(args.source, args.destination)
    except (OSError, ValueError, RuntimeError) as error:
        parser.exit(1, str(error) + "\n")
    print("Exported %d maintained files to %s" % (len(entries), args.destination))


if __name__ == "__main__":
    main()
# Change User Description Committed
#1 34004 Russell C. Jackson (Rusty) Improve SDP backup/recovery, Maintenance, logging and cloud operation.

Add owned checkpoint locks, recovery-aware retention, preserved database switches,
bounded cleanup and coordinated AWS/Azure snapshots. Fix service-user password
initialization, edge staging, standby promotion state parsing, proxy cache setup,
typed depot verification and historical-server configuration/error handling.
Remove unused OTP programs; keep Windows removed, server.locks cleanup and the
journal-count condition unchanged. Add no global shell strict modes.

Update operational documentation and add reproducible mkdirs.sh/mkdirs.cfg
container assurance. Validate Ubuntu 26.04 and UBI 8.10/9.8/10.2 across both Python
runtimes: 44 stages; 428 offline tests per runtime/OS, zero offline skips;
19 deployed-archive comparisons per combination. Three six-node topology runs
pass all 30 recovery/failure scenarios. P4D 2023.1 through 2026.1 on UBI 8/9
pass all 14 cells and 98 scenarios. Static, documentation and provenance checks
pass. Containers do not certify every fleet kernel, patch build or integration.

Tested source manifest SHA-256:
e1a23e17b8ced8c1148d9a250589977c6f6e98606bf2a81dc3ff5d7c909e3695
Deployed comparison archive SHA-256:
1bd23217356ee3808e040d39aea5be9519bda7c8f2b4924656a70aff8ad7293a