@mpgd/cli 0.36.0 → 0.36.1

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.
@@ -31,6 +31,27 @@ export interface CapacitorShellCommandRunner {
31
31
  }
32
32
  /** Materialize only missing native projects after the non-destructive plan is applied. */
33
33
  export declare function materializeCapacitorShellStarter(plan: CapacitorShellStarterPlan, runner?: CapacitorShellCommandRunner): void;
34
+ /**
35
+ * Backup rules for API < 31 (android:fullBackupContent). Every data domain is
36
+ * excluded so nothing from the app's private storage reaches a backup even
37
+ * if a game later turns android:allowBackup back on.
38
+ */
39
+ export declare const androidBackupRulesXml: string;
40
+ /**
41
+ * Data extraction rules for API 31+ (android:dataExtractionRules). Android 12
42
+ * ignores android:allowBackup="false" for device-to-device transfer on some
43
+ * devices, so cloud backup and device transfer each exclude every domain.
44
+ */
45
+ export declare const androidDataExtractionRulesXml: string;
46
+ /** Manifest application attributes that opt the shell out of backups. */
47
+ export declare const androidManifestBackupAttributes: readonly (readonly [string, string])[];
48
+ /**
49
+ * Set the backup opt-out attributes on the manifest application node and bind
50
+ * the tools namespace on the manifest root. Existing values are replaced in
51
+ * place; the attribute group keeps the indentation of the first attribute so
52
+ * the result is stable across repeated runs.
53
+ */
54
+ export declare function applyAndroidManifestBackupPolicy(manifest: string): string;
34
55
  /** Fail closed when a Release plist hardcodes values independently of Xcode build settings. */
35
56
  export declare function assertIosReleasePlistIdentity(source: string): void;
36
57
  export declare function assertAndroidReleaseDisplayName(shellApp: string, expectedDisplayName: string): void;
@@ -450,10 +450,14 @@ export function materializeCapacitorShellStarter(plan, runner = { run: runComman
450
450
  runner.run('pnpm', ['install', '--no-frozen-lockfile'], plan.gameRoot);
451
451
  runner.run('pnpm', ['--dir', shellPath, 'install', '--ignore-workspace', '--no-frozen-lockfile'], plan.gameRoot);
452
452
  for (const platform of plan.nativePlatformsToAdd) {
453
- if (!existsSync(path.join(plan.gameRoot, shellPath, platform))) {
453
+ const generatedNow = !existsSync(path.join(plan.gameRoot, shellPath, platform));
454
+ if (generatedNow) {
454
455
  runner.run('pnpm', ['--dir', shellPath, 'cap', 'add', platform], plan.gameRoot);
455
456
  }
456
457
  const manifest = readJsonObject(path.join(plan.gameRoot, shellPath, 'mpgd.native-shell.json'));
458
+ if (platform === 'android' && generatedNow) {
459
+ materializeAndroidBackupPolicy(plan.gameRoot);
460
+ }
457
461
  if (platform === 'ios') {
458
462
  materializeIosArchiveScheme(plan.gameRoot);
459
463
  const relative = `${shellPath}/ios/App/App/Info-Smoke.plist`;
@@ -473,6 +477,122 @@ export function materializeCapacitorShellStarter(plan, runner = { run: runComman
473
477
  throw new Error('Capacitor shell changed during installation; rerun the initializer.');
474
478
  }
475
479
  }
480
+ const androidToolsNamespace = 'http://schemas.android.com/tools';
481
+ const androidBackupDomains = ['root', 'file', 'database', 'sharedpref', 'external'];
482
+ const androidBackupExcludes = androidBackupDomains
483
+ .map((domain) => `<exclude domain="${domain}" path="." />`);
484
+ /**
485
+ * Backup rules for API < 31 (android:fullBackupContent). Every data domain is
486
+ * excluded so nothing from the app's private storage reaches a backup even
487
+ * if a game later turns android:allowBackup back on.
488
+ */
489
+ export const androidBackupRulesXml = [
490
+ '<?xml version="1.0" encoding="utf-8"?>',
491
+ '<!--',
492
+ ' Generated by mpgd target init capacitor. WebView storage and the',
493
+ ' game-services SharedPreferences save store hold player data, so the whole',
494
+ ' private data tree stays out of Android backups.',
495
+ '-->',
496
+ '<full-backup-content>',
497
+ ...androidBackupExcludes.map((line) => ` ${line}`),
498
+ '</full-backup-content>',
499
+ '',
500
+ ].join('\n');
501
+ /**
502
+ * Data extraction rules for API 31+ (android:dataExtractionRules). Android 12
503
+ * ignores android:allowBackup="false" for device-to-device transfer on some
504
+ * devices, so cloud backup and device transfer each exclude every domain.
505
+ */
506
+ export const androidDataExtractionRulesXml = [
507
+ '<?xml version="1.0" encoding="utf-8"?>',
508
+ '<!--',
509
+ ' Generated by mpgd target init capacitor. WebView storage and the',
510
+ ' game-services SharedPreferences save store hold player data, so neither',
511
+ ' cloud backup nor Android 12+ device-to-device transfer may copy them.',
512
+ '-->',
513
+ '<data-extraction-rules>',
514
+ ' <cloud-backup>',
515
+ ...androidBackupExcludes.map((line) => ` ${line}`),
516
+ ' </cloud-backup>',
517
+ ' <device-transfer>',
518
+ ...androidBackupExcludes.map((line) => ` ${line}`),
519
+ ' </device-transfer>',
520
+ '</data-extraction-rules>',
521
+ '',
522
+ ].join('\n');
523
+ /** Manifest application attributes that opt the shell out of backups. */
524
+ export const androidManifestBackupAttributes = [
525
+ ['android:allowBackup', 'false'],
526
+ ['android:fullBackupContent', '@xml/backup_rules'],
527
+ ['android:dataExtractionRules', '@xml/data_extraction_rules'],
528
+ ['tools:targetApi', 's'],
529
+ ];
530
+ /**
531
+ * Opt a freshly generated Android project out of cloud backups and Android
532
+ * 12+ device-to-device transfers. The Capacitor template enables
533
+ * android:allowBackup, which would copy WebView storage and the game-services
534
+ * SharedPreferences save store into Google backups; only a project created by
535
+ * this run is touched, never an existing game-owned shell.
536
+ */
537
+ function materializeAndroidBackupPolicy(gameRoot) {
538
+ const manifestFile = safeDestination(gameRoot, `${shellPath}/android/app/src/main/AndroidManifest.xml`);
539
+ if (!existsSync(manifestFile) || !lstatSync(manifestFile).isFile()) {
540
+ return;
541
+ }
542
+ const resources = [
543
+ ['backup_rules.xml', androidBackupRulesXml],
544
+ ['data_extraction_rules.xml', androidDataExtractionRulesXml],
545
+ ];
546
+ for (const [name, content] of resources) {
547
+ const resource = safeDestination(gameRoot, `${shellPath}/android/app/src/main/res/xml/${name}`);
548
+ if (!existsSync(resource)) {
549
+ mkdirSync(path.dirname(resource), { recursive: true });
550
+ writeFileSync(resource, content);
551
+ }
552
+ }
553
+ const source = readFileSync(manifestFile, 'utf8');
554
+ const hardened = applyAndroidManifestBackupPolicy(source);
555
+ if (hardened !== source) {
556
+ writeFileSync(manifestFile, hardened);
557
+ }
558
+ }
559
+ function xmlAttributePattern(name) {
560
+ return new RegExp(`(\\s*)${name.replaceAll('.', '\\.')}\\s*=\\s*(?:"[^"]*"|'[^']*')`, 'u');
561
+ }
562
+ /**
563
+ * Set the backup opt-out attributes on the manifest application node and bind
564
+ * the tools namespace on the manifest root. Existing values are replaced in
565
+ * place; the attribute group keeps the indentation of the first attribute so
566
+ * the result is stable across repeated runs.
567
+ */
568
+ export function applyAndroidManifestBackupPolicy(manifest) {
569
+ const root = /<manifest\b[^>]*?>/u.exec(manifest);
570
+ const application = /<application\b[^>]*?\/?>/u.exec(manifest);
571
+ if (root === null || application === null || application.index < root.index) {
572
+ return manifest;
573
+ }
574
+ let rootTag = root[0];
575
+ if (!/\sxmlns:tools\s*=/u.test(rootTag)) {
576
+ const androidNamespace = /\sxmlns:android\s*=\s*(?:"[^"]*"|'[^']*')/u.exec(rootTag);
577
+ const toolsNamespace = ` xmlns:tools="${androidToolsNamespace}"`;
578
+ rootTag = androidNamespace === null
579
+ ? rootTag.replace(/^<manifest\b/u, `<manifest${toolsNamespace}`)
580
+ : `${rootTag.slice(0, androidNamespace.index + androidNamespace[0].length)}${toolsNamespace}${rootTag.slice(androidNamespace.index + androidNamespace[0].length)}`;
581
+ }
582
+ let applicationTag = application[0];
583
+ const existing = xmlAttributePattern('android:allowBackup').exec(applicationTag);
584
+ const separator = existing?.[1] !== undefined && existing[1] !== ''
585
+ ? existing[1]
586
+ : (/^<application(\s+)\S/u.exec(applicationTag)?.[1] ?? ' ');
587
+ for (const [name] of androidManifestBackupAttributes) {
588
+ applicationTag = applicationTag.replace(xmlAttributePattern(name), '');
589
+ }
590
+ const attributes = androidManifestBackupAttributes
591
+ .map(([name, value]) => `${separator}${name}="${value}"`)
592
+ .join('');
593
+ applicationTag = applicationTag.replace(/^<application\b/u, `<application${attributes}`);
594
+ return `${manifest.slice(0, root.index)}${rootTag}${manifest.slice(root.index + root[0].length, application.index)}${applicationTag}${manifest.slice(application.index + application[0].length)}`;
595
+ }
476
596
  function materializeIosArchiveScheme(gameRoot) {
477
597
  const schemeRelative = `${shellPath}/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme`;
478
598
  const schemeFile = safeDestination(gameRoot, schemeRelative);
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitGitSha": "e78c277284a382f7007e5278d6e026fe030c18af",
2
+ "kitGitSha": "646745bcd2b326e05dfb7030e8c15d8a102e054b",
3
3
  "kitDirty": false,
4
- "packageVersion": "0.36.0"
4
+ "packageVersion": "0.36.1"
5
5
  }