UEPyScripts 1.4.0__tar.gz → 1.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/PKG-INFO +1 -1
  2. uepyscripts-1.4.2/src/uepyscripts/tools/ue/__init__.py +0 -0
  3. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ugs/git_utils.py +20 -1
  4. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ugs/push.py +84 -16
  5. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ugs/ugs_types.py +10 -0
  6. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/.gitignore +0 -0
  7. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/LICENSE +0 -0
  8. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/README.md +0 -0
  9. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/pyproject.toml +0 -0
  10. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/__init__.py +0 -0
  11. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/context/__init__.py +0 -0
  12. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/internal/__init__.py +0 -0
  13. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/internal/config.py +0 -0
  14. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/internal/engine.py +0 -0
  15. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/internal/engine_resolver.py +0 -0
  16. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/internal/project.py +0 -0
  17. /uepyscripts-1.4.0/src/uepyscripts/run/__init__.py → /uepyscripts-1.4.2/src/uepyscripts/py.typed +0 -0
  18. {uepyscripts-1.4.0/src/uepyscripts/tools → uepyscripts-1.4.2/src/uepyscripts/run}/__init__.py +0 -0
  19. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/run/buildgraph.py +0 -0
  20. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/run/uat.py +0 -0
  21. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/run/ubt.py +0 -0
  22. {uepyscripts-1.4.0/src/uepyscripts/tools/ue → uepyscripts-1.4.2/src/uepyscripts/tools}/__init__.py +0 -0
  23. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ci/buildgraph.py +0 -0
  24. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ci/cleanup.py +0 -0
  25. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/helpers.py +0 -0
  26. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/subprocess.py +0 -0
  27. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ue/check_engine_installation.py +0 -0
  28. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ue/close_editor.py +0 -0
  29. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ue/compile_editor.py +0 -0
  30. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ue/engine_installation/engine_destination.py +0 -0
  31. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ue/engine_installation/engine_installer.py +0 -0
  32. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ue/engine_installation/engine_source.py +0 -0
  33. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ue/generate_solution.py +0 -0
  34. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ue/run_editor.py +0 -0
  35. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ugs/cleanup.py +0 -0
  36. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/ugs/pull.py +0 -0
  37. {uepyscripts-1.4.0 → uepyscripts-1.4.2}/src/uepyscripts/tools/winreg.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: UEPyScripts
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: Collection of Python tools and helpers designed to automate common Unreal Engine project tasks (builds, packaging, editor control, CI tasks, etc.).
5
5
  Project-URL: Changelog, https://github.com/TheEmidee/UEPyScripts/releases
6
6
  Project-URL: Homepage, https://github.com/TheEmidee/UEPyScripts
File without changes
@@ -2,6 +2,8 @@ import os
2
2
  import subprocess
3
3
  from pathlib import Path
4
4
 
5
+ from uepyscripts import logger
6
+
5
7
  ANCESTRY_LOOKBACK = 2000
6
8
 
7
9
 
@@ -16,7 +18,24 @@ def get_current_branch(cwd: Path) -> str:
16
18
  branch = os.environ.get("GIT_BRANCH") # Jenkins' Git plugin sets this, sometimes as "origin/<branch>"
17
19
  if branch:
18
20
  return branch.split("/", 1)[1] if "/" in branch else branch
19
- return subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, text=True).strip()
21
+
22
+ branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, text=True).strip()
23
+ if branch != "HEAD":
24
+ return branch
25
+
26
+ # Detached HEAD (e.g. a CI checkout of an explicit ref rather than a
27
+ # tracked branch, with no GIT_BRANCH env var set either) — fall back to
28
+ # whichever local or remote-tracking ref currently points at this commit.
29
+ refs = subprocess.check_output(
30
+ ["git", "for-each-ref", "--points-at=HEAD", "--format=%(refname:short)", "refs/heads/", "refs/remotes/"],
31
+ cwd=cwd,
32
+ text=True,
33
+ ).splitlines()
34
+ for ref in refs:
35
+ return ref.split("/", 1)[1] if "/" in ref else ref
36
+
37
+ logger.warning("Could not resolve current branch: HEAD is detached and no ref points at it")
38
+ return branch
20
39
 
21
40
 
22
41
  def get_local_ancestry(cwd: Path, limit: int = ANCESTRY_LOOKBACK) -> list[str]:
@@ -35,10 +35,9 @@ from uepyscripts.tools.ugs.git_utils import (
35
35
  get_local_ancestry,
36
36
  resolve_nearest_published_ancestor,
37
37
  )
38
- from uepyscripts.tools.ugs.ugs_types import Manifest, VersionManifest
38
+ from uepyscripts.tools.ugs.ugs_types import HashCacheEntry, HashCacheManifest, Manifest, VersionManifest
39
39
 
40
40
  # ---- CONFIG ----------------------------------------------------------
41
- LOCAL_HASH_CACHE = ".hash-cache.json"
42
41
 
43
42
  # Extensions that go through the delta/checkpoint sync pipeline (no PDBs — see symstore below)
44
43
  SYNC_EXT = {".dll", ".exe", ".lib", ".modules", ".target"}
@@ -72,18 +71,18 @@ class HashCacheInfos:
72
71
 
73
72
 
74
73
  class HashCache:
75
- def __init__(self) -> None:
74
+ def __init__(self, commit_sha: str) -> None:
75
+ self.commit_sha = commit_sha
76
76
  self.cache: dict[str, HashCacheInfos] = dict()
77
- self.local_hash_cache = Path(LOCAL_HASH_CACHE)
78
77
 
79
- if self.local_hash_cache.exists():
80
- raw = json.loads(self.local_hash_cache.read_text())
81
- self.cache = {path: HashCacheInfos(**entry) for path, entry in raw.items()}
78
+ def seed_from(self, files: dict[str, HashCacheEntry]) -> None:
79
+ self.cache = {path: HashCacheInfos(**entry) for path, entry in files.items()}
82
80
 
83
- def save(self) -> None:
84
- self.local_hash_cache.parent.mkdir(parents=True, exist_ok=True)
85
- serializable = {path: asdict(entry) for path, entry in self.cache.items()}
86
- self.local_hash_cache.write_text(json.dumps(serializable, indent=2, sort_keys=True))
81
+ def to_manifest(self) -> HashCacheManifest:
82
+ return {
83
+ "commit_sha": self.commit_sha,
84
+ "files": {path: cast(HashCacheEntry, asdict(entry)) for path, entry in self.cache.items()},
85
+ }
87
86
 
88
87
  def get(self, path: str) -> HashCacheInfos | None:
89
88
  return self.cache.get(path)
@@ -102,10 +101,35 @@ class Context:
102
101
  )
103
102
  self.symbol_store_path: str = args.symbol_store_path
104
103
  self.tmp_root: Path = Path(tempfile.mkdtemp(prefix="publish-"))
105
- self.hash_cache = HashCache()
106
104
  self.root_path = root_path
107
105
  self.current_sha = get_current_sha(root_path)
108
106
  self.current_branch = get_current_branch(root_path)
107
+ self.ancestry: list[str] = get_local_ancestry(root_path)
108
+ self.hash_cache, self.hash_cache_index = self._load_hash_cache()
109
+
110
+ def _load_hash_cache(self) -> tuple[HashCache, list[str]]:
111
+ """Resolves the nearest commit with an uploaded hash cache and seeds a
112
+ fresh HashCache from it. Unlike the manifest baseline used for diffing,
113
+ an inexact match is fine here: a fingerprint mismatch just falls back to
114
+ a full hash for that file, no worse than a cold-cache case, so
115
+ there's no correctness risk in seeding from a non-ancestor."""
116
+ bucket_name = self.args.s3_bucket_name
117
+ hash_cache_index = cast(list[str], self.s3_client.download_json(bucket_name, "hash_caches/index.json", default=[]))
118
+ baseline_sha, _ = resolve_nearest_published_ancestor(hash_cache_index, self.ancestry)
119
+
120
+ hash_cache = HashCache(self.current_sha)
121
+ if baseline_sha is None:
122
+ logger.info("No hash cache baseline found — starting with a cold hash cache")
123
+ return hash_cache, hash_cache_index
124
+
125
+ try:
126
+ manifest = cast(HashCacheManifest, self.s3_client.download_json(bucket_name, f"hash_caches/{baseline_sha}.json"))
127
+ hash_cache.seed_from(manifest["files"])
128
+ logger.info(f"Seeded hash cache from {baseline_sha} ({len(manifest['files'])} entries)")
129
+ except Exception as e:
130
+ logger.warning(f"Failed to load hash cache from hash_caches/{baseline_sha}.json ({e}); starting with a cold hash cache")
131
+
132
+ return hash_cache, hash_cache_index
109
133
 
110
134
  def finalize(self) -> None:
111
135
  if not self.args.keep_temp_directory:
@@ -327,6 +351,24 @@ def prune_old_versions(context: Context, cfg: FilesConfiguration, index: list[st
327
351
  return index[cutoff_idx:], checkpoints_to_keep
328
352
 
329
353
 
354
+ def prune_old_hash_caches(context: Context, hash_cache_index: list[str], keep: int) -> list[str]:
355
+ """Simple count-based retention for the hash-cache side-channel: unlike
356
+ index.json (only appended on actual content changes), hash_cache_index
357
+ grows once per run, so it needs its own, faster-turnover retention
358
+ rather than piggybacking on prune_old_versions's checkpoint-aligned
359
+ logic."""
360
+ if len(hash_cache_index) <= keep:
361
+ return hash_cache_index
362
+
363
+ to_delete = hash_cache_index[:-keep]
364
+ to_keep = hash_cache_index[-keep:]
365
+
366
+ logger.info(f"Pruning {len(to_delete)} old hash cache(s)")
367
+ context.s3_client.delete_keys(context.args.s3_bucket_name, [f"hash_caches/{sha}.json" for sha in to_delete])
368
+
369
+ return to_keep
370
+
371
+
330
372
  def publish_to_symbol_store(context: Context, paths: list[str], cfg: FilesConfiguration) -> None:
331
373
  """Indexes PDB/DLL/EXE files into the symbol store via symstore.exe."""
332
374
  logger.info("Publish to symbol store")
@@ -397,6 +439,12 @@ def parse_arguments() -> argparse.Namespace:
397
439
  default=2,
398
440
  help="Number of checkpoints to retain; older ones (and their deltas) get pruned (default: 2)",
399
441
  )
442
+ parser.add_argument(
443
+ "--keep_hash_caches",
444
+ type=int,
445
+ default=100,
446
+ help="Number of hash cache entries to retain in hash_caches/index.json; older ones get pruned (default: 100)",
447
+ )
400
448
  parser.add_argument(
401
449
  "--checkpoint-branch",
402
450
  type=str,
@@ -460,8 +508,7 @@ def main() -> None:
460
508
  logger.info(f"Finished scan. Found {len(new_state)} files.")
461
509
 
462
510
  logger.info("Resolve diff baseline from git ancestry")
463
- ancestry = get_local_ancestry(context.root_path)
464
- baseline_sha, is_exact = resolve_nearest_published_ancestor(index, ancestry)
511
+ baseline_sha, is_exact = resolve_nearest_published_ancestor(index, context.ancestry)
465
512
 
466
513
  old_state: dict[str, str]
467
514
  if baseline_sha is None:
@@ -573,8 +620,29 @@ def main() -> None:
573
620
 
574
621
  logger.info("=== Finished processing binary files ===")
575
622
 
576
- logger.info("Save hash cache")
577
- context.hash_cache.save()
623
+ if not context.args.skip_upload:
624
+ logger.info("Upload hash cache")
625
+ context.s3_client.upload_bytes(
626
+ context.args.s3_bucket_name,
627
+ f"hash_caches/{context.current_sha}.json",
628
+ json.dumps(context.hash_cache.to_manifest()).encode(),
629
+ content_type="application/json",
630
+ )
631
+
632
+ if context.current_sha not in context.hash_cache_index:
633
+ context.hash_cache_index.append(context.current_sha)
634
+
635
+ context.hash_cache_index = prune_old_hash_caches(context, context.hash_cache_index, context.args.keep_hash_caches)
636
+
637
+ logger.info("Upload hash_caches/index.json")
638
+ context.s3_client.upload_bytes(
639
+ context.args.s3_bucket_name,
640
+ "hash_caches/index.json",
641
+ json.dumps(context.hash_cache_index).encode(),
642
+ content_type="application/json",
643
+ )
644
+ else:
645
+ logger.info("Skipping hash cache upload (--skip-upload)")
578
646
  finally:
579
647
  context.finalize()
580
648
 
@@ -7,3 +7,13 @@ class VersionManifest(TypedDict):
7
7
  files: Manifest # full state at this version: {relative_path: sha256}
8
8
  changed: list[str] # files added or modified since the previous version
9
9
  removed: list[str] # files removed since the previous version
10
+
11
+
12
+ class HashCacheEntry(TypedDict):
13
+ fingerprint: str # "mtime_ns:size"
14
+ hash: str # blake3 hex digest
15
+
16
+
17
+ class HashCacheManifest(TypedDict):
18
+ commit_sha: str # commit this cache was captured at
19
+ files: dict[str, HashCacheEntry]
File without changes
File without changes
File without changes
File without changes