#!/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 |