NikGapps 3.96__tar.gz → 3.97__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 (95) hide show
  1. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/build/Build.py +2 -0
  2. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/Assets.py +1 -1
  3. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/Config.py +5 -0
  4. {nikgapps-3.96 → nikgapps-3.97/NikGapps.egg-info}/PKG-INFO +2 -2
  5. {nikgapps-3.96 → nikgapps-3.97}/NikGapps.egg-info/requires.txt +1 -1
  6. {nikgapps-3.96/NikGapps.egg-info → nikgapps-3.97}/PKG-INFO +2 -2
  7. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/cli.py +99 -1
  8. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/config.py +9 -2
  9. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/consumer.py +63 -5
  10. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/gitlab.py +19 -3
  11. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/models.py +12 -3
  12. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/pipeline.py +116 -13
  13. nikgapps-3.97/nikgapps_registry/cli.py +169 -0
  14. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_registry/service.py +31 -1
  15. {nikgapps-3.96 → nikgapps-3.97}/setup.py +2 -2
  16. {nikgapps-3.96 → nikgapps-3.97}/tests_package_pipeline/test_consumer.py +37 -0
  17. {nikgapps-3.96 → nikgapps-3.97}/tests_package_pipeline/test_pipeline.py +34 -1
  18. {nikgapps-3.96 → nikgapps-3.97}/tests_registry/test_registry.py +14 -0
  19. nikgapps-3.96/nikgapps_registry/cli.py +0 -98
  20. {nikgapps-3.96 → nikgapps-3.97}/LICENSE +0 -0
  21. {nikgapps-3.96 → nikgapps-3.97}/MANIFEST.in +0 -0
  22. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/__init__.py +0 -0
  23. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/build/NikGappsManager.py +0 -0
  24. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/build/NikGappsOverlays.py +0 -0
  25. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/build/PackageConstants.py +0 -0
  26. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/build/Release.py +0 -0
  27. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/build/__init__.py +0 -0
  28. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/build_config.py +0 -0
  29. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/cache.py +0 -0
  30. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/config/NikGappsConfig.py +0 -0
  31. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/config/__init__.py +0 -0
  32. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/config_upload.py +0 -0
  33. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/copy_repos.py +0 -0
  34. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/AppSet.py +0 -0
  35. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/Args.py +0 -0
  36. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/ConfigObj.py +0 -0
  37. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/Package.py +0 -0
  38. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/__init__.py +0 -0
  39. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/addon.sh +0 -0
  40. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/appsets.json +0 -0
  41. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/changelogs.yaml +0 -0
  42. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/config_versions.json +0 -0
  43. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/customize.sh +0 -0
  44. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/debloater.config +0 -0
  45. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/functions.sh +0 -0
  46. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/gofile.sh +0 -0
  47. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/header.sh +0 -0
  48. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/installer.sh +0 -0
  49. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/magisk-update-binary.sh +0 -0
  50. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/module.prop +0 -0
  51. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/mount.sh +0 -0
  52. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/mtg_mount.sh +0 -0
  53. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/nikgapps.config +0 -0
  54. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/nikgapps_functions.sh +0 -0
  55. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/packages.json +0 -0
  56. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/uninstaller.sh +0 -0
  57. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/assets/unmount.sh +0 -0
  58. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/compression/CompOps.py +0 -0
  59. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/compression/Export.py +0 -0
  60. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/compression/__init__.py +0 -0
  61. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/git/TestGit.py +0 -0
  62. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/git/__init__.py +0 -0
  63. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/overlay/ApkMetaInfo.py +0 -0
  64. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/overlay/Bool.py +0 -0
  65. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/overlay/Library.py +0 -0
  66. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/overlay/Manifest.py +0 -0
  67. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/overlay/Overlay.py +0 -0
  68. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/overlay/Resources.py +0 -0
  69. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/overlay/String.py +0 -0
  70. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/overlay/StringArray.py +0 -0
  71. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/helper/overlay/__init__.py +0 -0
  72. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/main.py +0 -0
  73. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/overlay_control.py +0 -0
  74. {nikgapps-3.96 → nikgapps-3.97}/NikGapps/test.py +0 -0
  75. {nikgapps-3.96 → nikgapps-3.97}/NikGapps.egg-info/SOURCES.txt +0 -0
  76. {nikgapps-3.96 → nikgapps-3.97}/NikGapps.egg-info/dependency_links.txt +0 -0
  77. {nikgapps-3.96 → nikgapps-3.97}/NikGapps.egg-info/entry_points.txt +0 -0
  78. {nikgapps-3.96 → nikgapps-3.97}/NikGapps.egg-info/top_level.txt +0 -0
  79. {nikgapps-3.96 → nikgapps-3.97}/README.md +0 -0
  80. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/__init__.py +0 -0
  81. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/__main__.py +0 -0
  82. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/apk.py +0 -0
  83. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/archive.py +0 -0
  84. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/compat.py +0 -0
  85. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/errors.py +0 -0
  86. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/scanner.py +0 -0
  87. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/upgrade.py +0 -0
  88. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_package_pipeline/util.py +0 -0
  89. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_registry/__init__.py +0 -0
  90. {nikgapps-3.96 → nikgapps-3.97}/nikgapps_registry/__main__.py +0 -0
  91. {nikgapps-3.96 → nikgapps-3.97}/setup.cfg +0 -0
  92. {nikgapps-3.96 → nikgapps-3.97}/tests_package_pipeline/__init__.py +0 -0
  93. {nikgapps-3.96 → nikgapps-3.97}/tests_package_pipeline/test_overlay_control.py +0 -0
  94. {nikgapps-3.96 → nikgapps-3.97}/tests_package_pipeline/test_upgrade.py +0 -0
  95. {nikgapps-3.96 → nikgapps-3.97}/tests_registry/__init__.py +0 -0
@@ -34,6 +34,8 @@ class Build:
34
34
  Path(Config.PACKAGE_CACHE),
35
35
  channel=Config.PACKAGE_CHANNEL,
36
36
  channel_overrides=channel_overrides_from_env(Config.PACKAGE_CHANNEL_OVERRIDES),
37
+ release_index_url=Config.PACKAGE_RELEASE_INDEX_URL,
38
+ release_id=Config.PACKAGE_RELEASE_ID,
37
39
  token=os.environ.get("GITLAB_TOKEN"),
38
40
  )
39
41
  Config.APK_SOURCE = str(registry_source.prepare(
@@ -22,7 +22,7 @@ class Assets:
22
22
  cwd = assets_folder + os.path.sep
23
23
  system_name = A.system_name
24
24
  apksigner_path = A.get("apksigner.jar")
25
- apktool_path = A.get("apktool_2.11.1.jar")
25
+ apktool_path = A.get("apktool_3.0.2.jar")
26
26
  key_path = A.get("cert.pk8")
27
27
  cert_path = A.get("cert.x509.pem")
28
28
  private_key_pem = A.get("private_key.pem")
@@ -65,6 +65,11 @@ PACKAGE_APPSETS_URL = os.environ.get(
65
65
  "PACKAGE_APPSETS_URL",
66
66
  "https://gitlab.com/nikgapps/nikgapps-package-catalog/-/raw/main/appsets.json"
67
67
  )
68
+ PACKAGE_RELEASE_INDEX_URL = os.environ.get(
69
+ "PACKAGE_RELEASE_INDEX_URL",
70
+ "https://gitlab.com/nikgapps/nikgapps-package-catalog/-/raw/main/releases/index.json"
71
+ )
72
+ PACKAGE_RELEASE_ID = os.environ.get("PACKAGE_RELEASE_ID")
68
73
  PACKAGE_CACHE = os.environ.get(
69
74
  "PACKAGE_CACHE",
70
75
  os.path.abspath(os.path.join(os.getcwd(), ".nikgapps-package-cache"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: NikGapps
3
- Version: 3.96
3
+ Version: 3.97
4
4
  Summary: A short description of your project
5
5
  Home-page: https://github.com/nikgapps/project
6
6
  Author: Nikhil Menghani
@@ -22,7 +22,7 @@ Requires-Dist: PyGithub~=2.8.1
22
22
  Requires-Dist: python-gitlab~=8.0.0
23
23
  Requires-Dist: cryptography~=46.0.3
24
24
  Requires-Dist: python-dotenv~=1.2.1
25
- Requires-Dist: niklibrary~=0.55
25
+ Requires-Dist: niklibrary~=0.56
26
26
  Dynamic: author
27
27
  Dynamic: author-email
28
28
  Dynamic: classifier
@@ -10,4 +10,4 @@ PyGithub~=2.8.1
10
10
  python-gitlab~=8.0.0
11
11
  cryptography~=46.0.3
12
12
  python-dotenv~=1.2.1
13
- niklibrary~=0.55
13
+ niklibrary~=0.56
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: NikGapps
3
- Version: 3.96
3
+ Version: 3.97
4
4
  Summary: A short description of your project
5
5
  Home-page: https://github.com/nikgapps/project
6
6
  Author: Nikhil Menghani
@@ -22,7 +22,7 @@ Requires-Dist: PyGithub~=2.8.1
22
22
  Requires-Dist: python-gitlab~=8.0.0
23
23
  Requires-Dist: cryptography~=46.0.3
24
24
  Requires-Dist: python-dotenv~=1.2.1
25
- Requires-Dist: niklibrary~=0.55
25
+ Requires-Dist: niklibrary~=0.56
26
26
  Dynamic: author
27
27
  Dynamic: author-email
28
28
  Dynamic: classifier
@@ -24,6 +24,8 @@ DEFAULT_INPUT = Path(r"D:\workspace\python\16_stable")
24
24
  DEFAULT_OUTPUT = Path(r"D:\workspace\python\16_STABLE_PREBUILT_RELEASE")
25
25
  DEFAULT_CONFIG = Path(__file__).with_name("nikgapps-pipeline.example.json")
26
26
  DEFAULT_GITLAB_PROJECT = "85036487"
27
+ DEFAULT_RELEASE_ROOT = Path(__file__).resolve().parents[2]
28
+ DEFAULT_MULTI_RELEASE_OUTPUT = DEFAULT_RELEASE_ROOT / "MULTI_RELEASE_PREVIEW"
27
29
 
28
30
 
29
31
  def _load_env_file(path: Path) -> bool:
@@ -93,7 +95,13 @@ def build_parser() -> argparse.ArgumentParser:
93
95
  help="override platform API; otherwise derived from --android-version",
94
96
  )
95
97
  migrate.add_argument("--channel", default="stable")
98
+ migrate.add_argument("--source", default="legacy", help="payload source label recorded in release metadata")
96
99
  migrate.add_argument("--aapt2", default=find_aapt2())
100
+ migrate.add_argument(
101
+ "--overlay-root",
102
+ type=Path,
103
+ help="compiled overlay repository to embed into matching packages",
104
+ )
97
105
  migrate.add_argument(
98
106
  "--package",
99
107
  action="append",
@@ -161,6 +169,38 @@ def build_parser() -> argparse.ArgumentParser:
161
169
  )
162
170
  _common_gitlab(reset)
163
171
 
172
+ reset_sync = commands.add_parser(
173
+ "reset-and-sync",
174
+ help="permanently reset Generic packages, rebuild, publish, and replace metadata",
175
+ )
176
+ reset_sync.add_argument("--verbose", action="store_true")
177
+ reset_sync.add_argument("--confirm-project", required=True,
178
+ help="must exactly match --gitlab-project")
179
+ reset_sync.add_argument("--gitlab-project", default=DEFAULT_GITLAB_PROJECT)
180
+ reset_sync.add_argument("--input", type=Path, default=DEFAULT_INPUT,
181
+ help="advanced single-release input override")
182
+ reset_sync.add_argument("--output", type=Path, default=DEFAULT_MULTI_RELEASE_OUTPUT)
183
+ reset_sync.add_argument("--config", type=Path, default=DEFAULT_CONFIG)
184
+ reset_sync.add_argument(
185
+ "--android-version", default="16",
186
+ help="one version or comma-separated versions; discovers VERSION_stable and overlays_VERSION",
187
+ )
188
+ reset_sync.add_argument("--platform-api", type=int)
189
+ reset_sync.add_argument("--channel", default="stable")
190
+ reset_sync.add_argument("--source", default="legacy")
191
+ reset_sync.add_argument("--aapt2", default=find_aapt2())
192
+ reset_sync.add_argument("--overlay-root", type=Path)
193
+ reset_sync.add_argument(
194
+ "--release", action="append", metavar="VERSION=INPUT[=OVERLAYS]",
195
+ help="release to sync after one reset; repeat for Android 16 and 17",
196
+ )
197
+ reset_sync.add_argument("--package", action="append")
198
+ reset_sync.add_argument("--artifact-base-url")
199
+ reset_sync.add_argument("--metadata-branch", default="main")
200
+ reset_sync.add_argument("--compact", action="store_true")
201
+ reset_sync.add_argument("--compat-output", type=Path)
202
+ _common_gitlab(reset_sync)
203
+
164
204
  upgrade = commands.add_parser(
165
205
  "upgrade", help="create a stable legacy tree from a newer Mustang image"
166
206
  )
@@ -197,6 +237,7 @@ def _migrate(args: argparse.Namespace) -> int:
197
237
  android_version=args.android_version,
198
238
  platform_api=derived_platform_api,
199
239
  channel=args.channel,
240
+ source=args.source,
200
241
  )
201
242
  client: GitLabClient | None = None
202
243
  project = args.gitlab_project
@@ -237,7 +278,7 @@ def _migrate(args: argparse.Namespace) -> int:
237
278
  release_path = (
238
279
  f"releases/android-{config.android_version}-{config.architecture}.json"
239
280
  )
240
- for relative in ("catalog.json", "appsets.json", release_path):
281
+ for relative in ("catalog.json", "appsets.json", "releases/index.json", release_path):
241
282
  local = metadata / Path(relative)
242
283
  if local.exists():
243
284
  continue
@@ -267,6 +308,7 @@ def _migrate(args: argparse.Namespace) -> int:
267
308
  artifact_base_url=base_url,
268
309
  pretty=not args.compact,
269
310
  package_filter=set(args.package) if args.package else None,
311
+ overlay_root=args.overlay_root,
270
312
  )
271
313
  if args.compat_output:
272
314
  LegacyCompatibilityExporter().export(packages, args.compat_output)
@@ -314,6 +356,60 @@ def _reset_registry(args: argparse.Namespace) -> int:
314
356
  return 0
315
357
 
316
358
 
359
+ def _reset_and_sync(args: argparse.Namespace) -> int:
360
+ if args.confirm_project != str(args.gitlab_project):
361
+ raise PipelineError(
362
+ "--confirm-project must exactly match --gitlab-project; reset-and-sync is destructive"
363
+ )
364
+ releases: list[tuple[str, Path, Path | None]] = []
365
+ for value in args.release or []:
366
+ parts = value.split("=", 2)
367
+ if len(parts) not in (2, 3) or not parts[0] or not parts[1]:
368
+ raise PipelineError(
369
+ f"Invalid --release {value!r}; expected VERSION=INPUT[=OVERLAYS]"
370
+ )
371
+ releases.append((parts[0], Path(parts[1]), Path(parts[2]) if len(parts) == 3 else None))
372
+ if not releases:
373
+ versions = [value.strip() for value in str(args.android_version).split(",") if value.strip()]
374
+ if not versions:
375
+ raise PipelineError("--android-version must contain at least one Android version")
376
+ for android_version in versions:
377
+ releases.append((
378
+ android_version,
379
+ DEFAULT_RELEASE_ROOT / f"{android_version}_stable",
380
+ DEFAULT_RELEASE_ROOT / f"overlays_{android_version}",
381
+ ))
382
+ for android_version, input_path, overlay_root in releases:
383
+ if not input_path.is_dir():
384
+ raise PipelineError(f"Android {android_version} input directory does not exist: {input_path}")
385
+ if overlay_root is not None and not overlay_root.is_dir():
386
+ raise PipelineError(f"Android {android_version} overlay directory does not exist: {overlay_root}")
387
+
388
+ reset_result = _reset_registry(args)
389
+ if reset_result != 0:
390
+ return reset_result
391
+ # Force the only safe post-reset migration mode. These are intentionally
392
+ # not user-selectable flags on this command.
393
+
394
+ print(f"Starting fresh build and registry sync for {len(releases)} release(s)...")
395
+ for index, (android_version, input_path, overlay_root) in enumerate(releases):
396
+ operation = argparse.Namespace(**vars(args))
397
+ operation.android_version = android_version
398
+ operation.input = input_path
399
+ operation.overlay_root = overlay_root
400
+ operation.publish = True
401
+ operation.fresh_metadata = index == 0
402
+ operation.create_project = False
403
+ operation.namespace_id = None
404
+ operation.namespace = "nikgapps"
405
+ operation.visibility = "public"
406
+ print(f"[{index + 1}/{len(releases)}] Syncing Android {android_version} from {input_path}")
407
+ result = _migrate(operation)
408
+ if result != 0:
409
+ return result
410
+ return 0
411
+
412
+
317
413
  def main(argv: Sequence[str] | None = None) -> int:
318
414
  parser = build_parser()
319
415
  args = parser.parse_args(argv)
@@ -340,6 +436,8 @@ def main(argv: Sequence[str] | None = None) -> int:
340
436
  return 0
341
437
  if args.command == "reset-registry":
342
438
  return _reset_registry(args)
439
+ if args.command == "reset-and-sync":
440
+ return _reset_and_sync(args)
343
441
  if args.command == "upgrade":
344
442
  overlay_values = (args.android_version, args.overlay_source, args.overlay_output)
345
443
  if any(overlay_values) and not all(overlay_values):
@@ -34,7 +34,9 @@ class PipelineConfig:
34
34
  android_version: str = "16"
35
35
  platform_api: int = 36
36
36
  channel: str = "stable"
37
+ source: str = "legacy"
37
38
  architecture: str = "arm64-v8a"
39
+ default_device_type: str = "phone"
38
40
  default_partition: str = "product"
39
41
  packages: dict[str, PackageOverride] = field(default_factory=dict)
40
42
 
@@ -48,7 +50,7 @@ class PipelineConfig:
48
50
  def _override(value: dict[str, Any], default_partition: str) -> PackageOverride:
49
51
  accepted = {
50
52
  "id", "name", "primaryApk", "defaultPartition", "minApi", "maxApi",
51
- "architectures", "replaceable", "include", "exclude",
53
+ "architectures", "deviceTypes", "replaceable", "include", "exclude",
52
54
  "removeFiles", "removeOverlays", "privilegedPermissions",
53
55
  "cleanFlashOnly", "additionalInstallerScript", "validationScript",
54
56
  "addonIndex",
@@ -67,6 +69,7 @@ def _override(value: dict[str, Any], default_partition: str) -> PackageOverride:
67
69
  min_api=value.get("minApi"),
68
70
  max_api=value.get("maxApi"),
69
71
  architectures=value.get("architectures"),
72
+ device_types=value.get("deviceTypes"),
70
73
  replaceable=value.get("replaceable", True),
71
74
  include=value.get("include"),
72
75
  exclude=value.get("exclude", []),
@@ -81,13 +84,15 @@ def _override(value: dict[str, Any], default_partition: str) -> PackageOverride:
81
84
 
82
85
 
83
86
  def load_config(
84
- path: Path | None, *, android_version: str, platform_api: int, channel: str
87
+ path: Path | None, *, android_version: str, platform_api: int, channel: str,
88
+ source: str = "legacy",
85
89
  ) -> PipelineConfig:
86
90
  if path is None:
87
91
  return PipelineConfig(
88
92
  android_version=android_version,
89
93
  platform_api=platform_api,
90
94
  channel=channel,
95
+ source=source,
91
96
  )
92
97
  try:
93
98
  raw = json.loads(path.read_text(encoding="utf-8"))
@@ -105,7 +110,9 @@ def load_config(
105
110
  # The channel is an operation-time promotion target. A single config
106
111
  # must be reusable for stable, beta, and canary publication.
107
112
  channel=channel,
113
+ source=source,
108
114
  architecture=raw.get("architecture", "arm64-v8a"),
115
+ default_device_type=raw.get("defaultDeviceType", "phone"),
109
116
  default_partition=default_partition,
110
117
  )
111
118
  packages = raw.get("packages", {})
@@ -56,6 +56,8 @@ class CatalogPackageSource:
56
56
  *,
57
57
  channel: str = "stable",
58
58
  channel_overrides: dict[str, str] | None = None,
59
+ release_index_url: str | None = None,
60
+ release_id: str | None = None,
59
61
  token: str | None = None,
60
62
  ) -> None:
61
63
  self.catalog_url = catalog_url
@@ -63,6 +65,8 @@ class CatalogPackageSource:
63
65
  self.cache_directory = cache_directory.resolve()
64
66
  self.channel = channel
65
67
  self.channel_overrides = channel_overrides or {}
68
+ self.release_index_url = release_index_url
69
+ self.release_id = release_id
66
70
  self.token = token
67
71
 
68
72
  def prepare(
@@ -70,6 +74,7 @@ class CatalogPackageSource:
70
74
  appsets: Iterable[Any],
71
75
  android_version: str | int | float,
72
76
  architecture: str,
77
+ device_type: str = "phone",
73
78
  ) -> Path:
74
79
  appsets = list(appsets)
75
80
  selected_names = ", ".join(appset.title for appset in appsets)
@@ -80,6 +85,32 @@ class CatalogPackageSource:
80
85
  _message("Reading catalog and AppSet metadata")
81
86
  catalog = self._read_json(self.catalog_url)
82
87
  appset_document = self._read_json(self.appsets_url)
88
+ release_versions: dict[str, str] = {}
89
+ if self.release_index_url:
90
+ release_index = self._read_json(self.release_index_url)
91
+ arch = catalog_architecture(architecture)
92
+ selected_id = self.release_id
93
+ if not selected_id:
94
+ selected_id = (
95
+ release_index.get("latest", {}).get(str(android_version), {})
96
+ .get(self.channel, {}).get(arch)
97
+ )
98
+ summary = next((item for item in release_index.get("releases", [])
99
+ if item.get("id") == selected_id), None)
100
+ if not summary:
101
+ raise PipelineError(
102
+ f"No {self.channel} release for Android {android_version} ({arch})"
103
+ )
104
+ metadata_root = self.release_index_url.rsplit("/releases/index.json", 1)[0]
105
+ release = self._read_json(f"{metadata_root}/{summary['manifest']}")
106
+ release_versions = {
107
+ package_id: value["version"]
108
+ for package_id, value in release.get("packages", {}).items()
109
+ }
110
+ appset_document = {
111
+ "schemaVersion": release.get("schemaVersion", 1),
112
+ "appSets": release.get("appSets", []),
113
+ }
83
114
  catalog_packages = {
84
115
  item["id"]: item for item in catalog.get("packages", [])
85
116
  }
@@ -137,7 +168,10 @@ class CatalogPackageSource:
137
168
  package = catalog_packages.get(package_id)
138
169
  if not package:
139
170
  raise PipelineError(f"Resolved package {package_id!r} is absent from catalog.json")
140
- version = self._resolve_version(package, api, arch)
171
+ version = self._resolve_version(
172
+ package, api, arch, release_versions.get(package_id),
173
+ catalog.get("defaults", {}), device_type
174
+ )
141
175
  resolutions.append((package_id, package, version))
142
176
  fingerprint = hashlib.sha256(
143
177
  "\n".join(
@@ -157,9 +191,15 @@ class CatalogPackageSource:
157
191
  source_root.mkdir(parents=True, exist_ok=True)
158
192
  selected_appset_names = {appset.title for appset in appsets}
159
193
  for package_id, package, version in resolutions:
194
+ resolved_version_key = (
195
+ release_versions.get(package_id)
196
+ or package.get("channels", {}).get(
197
+ self.channel_overrides.get(package_id, self.channel)
198
+ )
199
+ or "unknown"
200
+ )
160
201
  _message(
161
- f"Resolving {package_id} -> "
162
- f"{package.get('channels', {}).get(self.channel, 'channel override')}"
202
+ f"Resolving {package_id} -> {resolved_version_key}"
163
203
  )
164
204
  archive = self._artifact(version["artifact"])
165
205
  self._materialize(
@@ -181,27 +221,45 @@ class CatalogPackageSource:
181
221
  package: dict[str, Any],
182
222
  api: int,
183
223
  architecture: str,
224
+ release_version: str | None = None,
225
+ defaults: dict[str, Any] | None = None,
226
+ device_type: str = "phone",
184
227
  ) -> dict[str, Any]:
185
228
  package_id = package["id"]
186
229
  channel = self.channel_overrides.get(package_id, self.channel)
187
- version_key = package.get("channels", {}).get(channel)
230
+ version_key = release_version or package.get("channels", {}).get(channel)
188
231
  if not version_key:
189
232
  raise PipelineError(f"Package {package_id} has no {channel!r} channel")
190
233
  version = package.get("versions", {}).get(version_key)
191
234
  if not version:
192
235
  raise PipelineError(f"Package {package_id} channel points to missing {version_key}")
193
236
  android = version.get("android", {})
237
+ supported_android = version.get("supportedAndroidVersions", [])
238
+ requested_android = {
239
+ 29: "10", 30: "11", 31: "12", 32: "12.1", 33: "13",
240
+ 34: "14", 35: "15", 36: "16", 37: "17",
241
+ }.get(api)
242
+ if supported_android and requested_android not in supported_android:
243
+ raise PipelineError(
244
+ f"Package {package_id} is not published for Android {requested_android or api}"
245
+ )
194
246
  minimum = android.get("minApi")
195
247
  maximum = android.get("maxApi")
196
248
  if minimum is not None and api < int(minimum):
197
249
  raise PipelineError(f"Package {package_id} requires API {minimum}; build targets API {api}")
198
250
  if maximum is not None and api > int(maximum):
199
251
  raise PipelineError(f"Package {package_id} supports at most API {maximum}; build targets API {api}")
200
- architectures = version.get("architectures", [])
252
+ defaults = defaults or {}
253
+ architectures = version.get("architectures", defaults.get("architectures", []))
201
254
  if architectures and architecture not in architectures:
202
255
  raise PipelineError(
203
256
  f"Package {package_id} does not support {architecture}: {architectures}"
204
257
  )
258
+ device_types = version.get("deviceTypes", defaults.get("deviceTypes", ["phone"]))
259
+ if device_types and device_type not in device_types:
260
+ raise PipelineError(
261
+ f"Package {package_id} does not support {device_type} devices: {device_types}"
262
+ )
205
263
  return version
206
264
 
207
265
  def _artifact(self, artifact: dict[str, Any]) -> Path:
@@ -216,9 +216,25 @@ class GitLabClient:
216
216
 
217
217
  def delete_package(self, project: str, package_id: int) -> None:
218
218
  encoded_project = urllib.parse.quote(str(project), safe="")
219
- self._request(
220
- "DELETE", f"projects/{encoded_project}/packages/{int(package_id)}"
221
- )
219
+ endpoint = f"projects/{encoded_project}/packages/{int(package_id)}"
220
+ last_error: PipelineError | None = None
221
+ for attempt in range(6):
222
+ try:
223
+ self._request("DELETE", endpoint)
224
+ return
225
+ except PipelineError as exc:
226
+ last_error = exc
227
+ retryable = any(f"HTTP {status}" in str(exc) for status in (429, 500, 502, 503, 504))
228
+ if not retryable or attempt == 5:
229
+ raise
230
+ delay = min(30, 2 ** attempt)
231
+ print(
232
+ f"[Registry] Delete rate-limited or temporarily unavailable "
233
+ f"(attempt {attempt + 1}/6); retrying in {delay}s"
234
+ )
235
+ time.sleep(delay)
236
+ assert last_error is not None
237
+ raise last_error
222
238
 
223
239
  def commit_directory(
224
240
  self,
@@ -31,6 +31,7 @@ class PackageOverride:
31
31
  min_api: int | None = None
32
32
  max_api: int | None = None
33
33
  architectures: list[str] | None = None
34
+ device_types: list[str] | None = None
34
35
  replaceable: bool = True
35
36
  include: list[str] | None = None
36
37
  exclude: list[str] = field(default_factory=list)
@@ -64,6 +65,7 @@ class BuiltPackage:
64
65
  max_api: int | None
65
66
  default_partition: str
66
67
  architectures: list[str]
68
+ device_types: list[str]
67
69
  primary_apk: str | None
68
70
  replaceable: bool
69
71
  files: list[PayloadFile]
@@ -75,8 +77,9 @@ class BuiltPackage:
75
77
  dependencies: list[dict[str, Any]] = field(default_factory=list)
76
78
  descriptor: dict[str, Any] = field(default_factory=dict)
77
79
 
78
- def catalog_version(self, artifact_url: str) -> dict[str, Any]:
79
- return {
80
+ def catalog_version(self, artifact_url: str, android_version: str, source: str,
81
+ default_architecture: str, default_device_type: str) -> dict[str, Any]:
82
+ result = {
80
83
  "versionName": self.metadata.version_name if self.metadata else self.version_key,
81
84
  "versionCode": self.metadata.version_code if self.metadata else 0,
82
85
  "packageName": self.metadata.package_name if self.metadata else None,
@@ -85,7 +88,8 @@ class BuiltPackage:
85
88
  "targetApi": self.target_api,
86
89
  "maxApi": self.max_api,
87
90
  },
88
- "architectures": self.architectures,
91
+ "supportedAndroidVersions": [str(android_version)],
92
+ "sources": [source],
89
93
  "defaultPartition": self.default_partition,
90
94
  "apk": {
91
95
  "path": self.primary_apk,
@@ -100,3 +104,8 @@ class BuiltPackage:
100
104
  "files": self.descriptor.get("files", []),
101
105
  "install": self.descriptor.get("install"),
102
106
  }
107
+ if self.architectures != [default_architecture]:
108
+ result["architectures"] = self.architectures
109
+ if self.device_types != [default_device_type]:
110
+ result["deviceTypes"] = self.device_types
111
+ return result