NikGapps 3.95__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.95 → nikgapps-3.97}/NikGapps/build/Build.py +2 -0
  2. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/Assets.py +1 -1
  3. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/Config.py +5 -0
  4. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/config_versions.json +3 -2
  5. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/overlay_control.py +15 -0
  6. {nikgapps-3.95 → nikgapps-3.97/NikGapps.egg-info}/PKG-INFO +2 -2
  7. {nikgapps-3.95 → nikgapps-3.97}/NikGapps.egg-info/requires.txt +1 -1
  8. {nikgapps-3.95/NikGapps.egg-info → nikgapps-3.97}/PKG-INFO +2 -2
  9. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/cli.py +118 -2
  10. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/config.py +9 -2
  11. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/consumer.py +63 -5
  12. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/gitlab.py +19 -3
  13. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/models.py +12 -3
  14. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/pipeline.py +148 -61
  15. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/upgrade.py +75 -22
  16. nikgapps-3.97/nikgapps_registry/cli.py +169 -0
  17. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_registry/service.py +31 -1
  18. {nikgapps-3.95 → nikgapps-3.97}/setup.py +2 -2
  19. {nikgapps-3.95 → nikgapps-3.97}/tests_package_pipeline/test_consumer.py +48 -16
  20. {nikgapps-3.95 → nikgapps-3.97}/tests_package_pipeline/test_overlay_control.py +13 -1
  21. {nikgapps-3.95 → nikgapps-3.97}/tests_package_pipeline/test_pipeline.py +46 -8
  22. {nikgapps-3.95 → nikgapps-3.97}/tests_package_pipeline/test_upgrade.py +38 -1
  23. {nikgapps-3.95 → nikgapps-3.97}/tests_registry/test_registry.py +14 -0
  24. nikgapps-3.95/nikgapps_registry/cli.py +0 -98
  25. {nikgapps-3.95 → nikgapps-3.97}/LICENSE +0 -0
  26. {nikgapps-3.95 → nikgapps-3.97}/MANIFEST.in +0 -0
  27. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/__init__.py +0 -0
  28. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/build/NikGappsManager.py +0 -0
  29. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/build/NikGappsOverlays.py +0 -0
  30. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/build/PackageConstants.py +0 -0
  31. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/build/Release.py +0 -0
  32. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/build/__init__.py +0 -0
  33. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/build_config.py +0 -0
  34. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/cache.py +0 -0
  35. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/config/NikGappsConfig.py +0 -0
  36. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/config/__init__.py +0 -0
  37. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/config_upload.py +0 -0
  38. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/copy_repos.py +0 -0
  39. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/AppSet.py +0 -0
  40. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/Args.py +0 -0
  41. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/ConfigObj.py +0 -0
  42. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/Package.py +0 -0
  43. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/__init__.py +0 -0
  44. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/addon.sh +0 -0
  45. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/appsets.json +0 -0
  46. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/changelogs.yaml +0 -0
  47. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/customize.sh +0 -0
  48. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/debloater.config +0 -0
  49. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/functions.sh +0 -0
  50. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/gofile.sh +0 -0
  51. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/header.sh +0 -0
  52. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/installer.sh +0 -0
  53. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/magisk-update-binary.sh +0 -0
  54. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/module.prop +0 -0
  55. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/mount.sh +0 -0
  56. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/mtg_mount.sh +0 -0
  57. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/nikgapps.config +0 -0
  58. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/nikgapps_functions.sh +0 -0
  59. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/packages.json +0 -0
  60. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/uninstaller.sh +0 -0
  61. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/assets/unmount.sh +0 -0
  62. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/compression/CompOps.py +0 -0
  63. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/compression/Export.py +0 -0
  64. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/compression/__init__.py +0 -0
  65. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/git/TestGit.py +0 -0
  66. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/git/__init__.py +0 -0
  67. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/overlay/ApkMetaInfo.py +0 -0
  68. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/overlay/Bool.py +0 -0
  69. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/overlay/Library.py +0 -0
  70. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/overlay/Manifest.py +0 -0
  71. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/overlay/Overlay.py +0 -0
  72. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/overlay/Resources.py +0 -0
  73. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/overlay/String.py +0 -0
  74. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/overlay/StringArray.py +0 -0
  75. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/helper/overlay/__init__.py +0 -0
  76. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/main.py +0 -0
  77. {nikgapps-3.95 → nikgapps-3.97}/NikGapps/test.py +0 -0
  78. {nikgapps-3.95 → nikgapps-3.97}/NikGapps.egg-info/SOURCES.txt +0 -0
  79. {nikgapps-3.95 → nikgapps-3.97}/NikGapps.egg-info/dependency_links.txt +0 -0
  80. {nikgapps-3.95 → nikgapps-3.97}/NikGapps.egg-info/entry_points.txt +0 -0
  81. {nikgapps-3.95 → nikgapps-3.97}/NikGapps.egg-info/top_level.txt +0 -0
  82. {nikgapps-3.95 → nikgapps-3.97}/README.md +0 -0
  83. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/__init__.py +0 -0
  84. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/__main__.py +0 -0
  85. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/apk.py +0 -0
  86. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/archive.py +0 -0
  87. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/compat.py +0 -0
  88. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/errors.py +0 -0
  89. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/scanner.py +0 -0
  90. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_package_pipeline/util.py +0 -0
  91. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_registry/__init__.py +0 -0
  92. {nikgapps-3.95 → nikgapps-3.97}/nikgapps_registry/__main__.py +0 -0
  93. {nikgapps-3.95 → nikgapps-3.97}/setup.cfg +0 -0
  94. {nikgapps-3.95 → nikgapps-3.97}/tests_package_pipeline/__init__.py +0 -0
  95. {nikgapps-3.95 → 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"))
@@ -6,5 +6,6 @@
6
6
  "13": "37",
7
7
  "14": "37",
8
8
  "15": "37",
9
- "16": "38"
10
- }
9
+ "16": "38",
10
+ "17": "39"
11
+ }
@@ -12,8 +12,22 @@ from niklibrary.git.Git import Git
12
12
  from NikGapps.helper.overlay.Overlay import Overlay
13
13
 
14
14
 
15
+ ANDROID_PLATFORM_VERSIONS = {
16
+ # niklibrary 0.28 predates Android 17. Keep the compatibility entry here
17
+ # until the same mapping is available in the shared library.
18
+ "17": {"sdk": "37", "code": "CinnamonBun"},
19
+ }
20
+
21
+
22
+ def register_android_platform(android_version):
23
+ version = str(android_version)
24
+ if version not in Statics.android_versions and version in ANDROID_PLATFORM_VERSIONS:
25
+ Statics.android_versions[version] = ANDROID_PLATFORM_VERSIONS[version]
26
+
27
+
15
28
  def build_local_overlays(android_version, source_dir, output_dir):
16
29
  """Generate and compile overlays locally without cloning or pushing repos."""
30
+ register_android_platform(android_version)
17
31
  source_dir = os.path.abspath(source_dir)
18
32
  output_dir = os.path.abspath(output_dir)
19
33
  os.makedirs(source_dir, exist_ok=True)
@@ -40,6 +54,7 @@ def overlay_control():
40
54
  android_versions = args.get_android_versions()
41
55
 
42
56
  for android_version in android_versions:
57
+ register_android_platform(android_version)
43
58
  repo_name = Statics.get_overlay_source_repo(android_version)
44
59
  repo_dir = Statics.get_overlay_source_directory(android_version)
45
60
  branch = "main"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: NikGapps
3
- Version: 3.95
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.95
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
  )
@@ -168,6 +208,11 @@ def build_parser() -> argparse.ArgumentParser:
168
208
  upgrade.add_argument("--baseline-firmware", type=Path, required=True)
169
209
  upgrade.add_argument("--target-firmware", type=Path, required=True)
170
210
  upgrade.add_argument("--output", type=Path, required=True)
211
+ upgrade.add_argument("--carry-forward-missing", action="store_true",
212
+ help="copy the complete template first, replace everything available from the target image, and audit retained files")
213
+ upgrade.add_argument("--android-version", help="build overlays for this Android version after upgrading")
214
+ upgrade.add_argument("--overlay-source", type=Path, help="generated overlay source workspace")
215
+ upgrade.add_argument("--overlay-output", type=Path, help="compiled overlay APK output")
171
216
  return parser
172
217
 
173
218
 
@@ -192,6 +237,7 @@ def _migrate(args: argparse.Namespace) -> int:
192
237
  android_version=args.android_version,
193
238
  platform_api=derived_platform_api,
194
239
  channel=args.channel,
240
+ source=args.source,
195
241
  )
196
242
  client: GitLabClient | None = None
197
243
  project = args.gitlab_project
@@ -232,7 +278,7 @@ def _migrate(args: argparse.Namespace) -> int:
232
278
  release_path = (
233
279
  f"releases/android-{config.android_version}-{config.architecture}.json"
234
280
  )
235
- for relative in ("catalog.json", "appsets.json", release_path):
281
+ for relative in ("catalog.json", "appsets.json", "releases/index.json", release_path):
236
282
  local = metadata / Path(relative)
237
283
  if local.exists():
238
284
  continue
@@ -262,6 +308,7 @@ def _migrate(args: argparse.Namespace) -> int:
262
308
  artifact_base_url=base_url,
263
309
  pretty=not args.compact,
264
310
  package_filter=set(args.package) if args.package else None,
311
+ overlay_root=args.overlay_root,
265
312
  )
266
313
  if args.compat_output:
267
314
  LegacyCompatibilityExporter().export(packages, args.compat_output)
@@ -309,6 +356,60 @@ def _reset_registry(args: argparse.Namespace) -> int:
309
356
  return 0
310
357
 
311
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
+
312
413
  def main(argv: Sequence[str] | None = None) -> int:
313
414
  parser = build_parser()
314
415
  args = parser.parse_args(argv)
@@ -335,10 +436,25 @@ def main(argv: Sequence[str] | None = None) -> int:
335
436
  return 0
336
437
  if args.command == "reset-registry":
337
438
  return _reset_registry(args)
439
+ if args.command == "reset-and-sync":
440
+ return _reset_and_sync(args)
338
441
  if args.command == "upgrade":
442
+ overlay_values = (args.android_version, args.overlay_source, args.overlay_output)
443
+ if any(overlay_values) and not all(overlay_values):
444
+ raise PipelineError("--android-version, --overlay-source, and --overlay-output must be supplied together")
445
+ overlays = []
446
+ if all(overlay_values):
447
+ from NikGapps.overlay_control import build_local_overlays
448
+ overlays = build_local_overlays(args.android_version, args.overlay_source, args.overlay_output)
339
449
  report = StableTreeUpgrader().upgrade(
340
- args.template, args.baseline_firmware, args.target_firmware, args.output
450
+ args.template, args.baseline_firmware, args.target_firmware, args.output,
451
+ carry_forward_missing=args.carry_forward_missing,
341
452
  )
453
+ if overlays:
454
+ report.built_overlays = [str(Path(path).resolve().relative_to(args.overlay_output.resolve())).replace("\\", "/")
455
+ for path in overlays]
456
+ (args.output / "upgrade-report.json").write_text(
457
+ json.dumps(report.as_dict(), indent=2) + "\n", encoding="utf-8")
342
458
  print(json.dumps(report.as_dict(), indent=2))
343
459
  return 0
344
460
  return _migrate(args)
@@ -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