@livx.cc/appwrap 0.42.1 → 0.42.3
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.
- package/package.json +1 -1
- package/runtime/App_Resources/iOS/PrivacyInfo.xcprivacy +11 -33
- package/src/cli.ts +130 -21
package/package.json
CHANGED
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
- FileTimestamp (C617.1) — NativeScript reads bundle/app file metadata at runtime
|
|
9
9
|
- SystemBootTime (35F9.1) — NativeScript timers/uptime
|
|
10
10
|
- DiskSpace (E174.1) — NativeScript file I/O
|
|
11
|
-
NSPrivacyTracking is false (no IDFA, no cross-app/third-party linkage)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
NSPrivacyTracking is false (no IDFA, no cross-app/third-party linkage) and NSPrivacyCollectedDataTypes
|
|
12
|
+
is EMPTY by default: the stock runtime does NOT collect or transmit any data. kit.context() merely
|
|
13
|
+
RETURNS install context (IDFV, platform, version) to the app on request — it never sends it anywhere —
|
|
14
|
+
so the default binary matches a "Data Not Collected" App Store privacy label.
|
|
15
|
+
|
|
16
|
+
If YOUR app wires analytics that actually transmits data (DeviceID/IDFV, ProductInteraction, account,
|
|
17
|
+
location, …) you MUST declare it here — replace this whole file via your app's overrides dir
|
|
18
|
+
(appwrap.overrides/App_Resources/iOS/PrivacyInfo.xcprivacy). `applyOverrides` copies overrides over
|
|
19
|
+
the generated native/ last, so your file wins. Add the matching NSPrivacyCollectedDataTypes entries
|
|
20
|
+
(and set NSPrivacyTracking=true if you track) — see tracking-analytics + store-readiness.
|
|
18
21
|
-->
|
|
19
22
|
<plist version="1.0">
|
|
20
23
|
<dict>
|
|
@@ -23,32 +26,7 @@
|
|
|
23
26
|
<key>NSPrivacyTrackingDomains</key>
|
|
24
27
|
<array/>
|
|
25
28
|
<key>NSPrivacyCollectedDataTypes</key>
|
|
26
|
-
<array
|
|
27
|
-
<dict>
|
|
28
|
-
<key>NSPrivacyCollectedDataType</key>
|
|
29
|
-
<string>NSPrivacyCollectedDataTypeDeviceID</string>
|
|
30
|
-
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
31
|
-
<false/>
|
|
32
|
-
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
33
|
-
<false/>
|
|
34
|
-
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
35
|
-
<array>
|
|
36
|
-
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
37
|
-
</array>
|
|
38
|
-
</dict>
|
|
39
|
-
<dict>
|
|
40
|
-
<key>NSPrivacyCollectedDataType</key>
|
|
41
|
-
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
|
|
42
|
-
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
43
|
-
<false/>
|
|
44
|
-
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
45
|
-
<false/>
|
|
46
|
-
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
47
|
-
<array>
|
|
48
|
-
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
49
|
-
</array>
|
|
50
|
-
</dict>
|
|
51
|
-
</array>
|
|
29
|
+
<array/>
|
|
52
30
|
<key>NSPrivacyAccessedAPITypes</key>
|
|
53
31
|
<array>
|
|
54
32
|
<dict>
|
package/src/cli.ts
CHANGED
|
@@ -592,7 +592,11 @@ function stampTeamId(outDir: string, cfg: AppwrapConfig, ctx?: { cwd: string; co
|
|
|
592
592
|
writeFileSync(xcconfig, src);
|
|
593
593
|
}
|
|
594
594
|
|
|
595
|
-
|
|
595
|
+
/** Profile kind, derived from the .mobileprovision payload: ProvisionsAllDevices → enterprise;
|
|
596
|
+
* ProvisionedDevices + get-task-allow → development; ProvisionedDevices w/o it → adhoc; else appstore. */
|
|
597
|
+
type ProvKind = 'development' | 'adhoc' | 'enterprise' | 'appstore';
|
|
598
|
+
|
|
599
|
+
interface ProvProfile { name: string; uuid: string; teamId: string; bundleId: string; exp: number; kind: ProvKind }
|
|
596
600
|
|
|
597
601
|
/** Discover provisioning profiles installed on this machine (both the legacy MobileDevice dir and
|
|
598
602
|
* the modern Xcode UserData dir). Each is decoded with `security cms` and reduced to the fields we
|
|
@@ -623,7 +627,11 @@ function findProvisioningProfiles(): ProvProfile[] {
|
|
|
623
627
|
if (!name || !uuid || !team || !appId || byUuid.has(uuid)) continue;
|
|
624
628
|
// application-identifier is `<TEAM>.<bundle.id>`; strip the team prefix to get the bundle id.
|
|
625
629
|
const bundleId = appId.startsWith(team + '.') ? appId.slice(team.length + 1) : appId;
|
|
626
|
-
|
|
630
|
+
const kind: ProvKind = /<key>ProvisionsAllDevices<\/key>\s*<true\s*\/>/.test(raw) ? 'enterprise'
|
|
631
|
+
: /<key>ProvisionedDevices<\/key>/.test(raw)
|
|
632
|
+
? (/<key>get-task-allow<\/key>\s*<true\s*\/>/.test(raw) ? 'development' : 'adhoc')
|
|
633
|
+
: 'appstore';
|
|
634
|
+
byUuid.set(uuid, { name, uuid, teamId: team, bundleId, exp: expStr ? Date.parse(expStr) : 0, kind });
|
|
627
635
|
} catch { /* skip unreadable profile */ }
|
|
628
636
|
}
|
|
629
637
|
}
|
|
@@ -635,10 +643,14 @@ function findProvisioningProfiles(): ProvProfile[] {
|
|
|
635
643
|
* pinned) → newest-expiry (headless). Returns null when nothing matches (caller warns + skips). */
|
|
636
644
|
function selectSigningProfile(
|
|
637
645
|
profiles: ProvProfile[], teamId: string, bundleId: string, cfg: AppwrapConfig,
|
|
638
|
-
ctx?: { configPath: string },
|
|
646
|
+
ctx?: { configPath: string }, kind: ProvKind | 'device' = 'device',
|
|
639
647
|
): string | null {
|
|
640
648
|
const now = Date.now();
|
|
641
|
-
|
|
649
|
+
// 'appstore' (store archive) must sign with an App Store distribution profile; 'device' (deploy)
|
|
650
|
+
// must NOT — an appstore profile can't install on a device. Filtering here is what keeps ONE
|
|
651
|
+
// signingProfiles pin map safe: a pin of the wrong kind simply doesn't match this lane.
|
|
652
|
+
const kindOk = (p: ProvProfile): boolean => (kind === 'device' ? p.kind !== 'appstore' : p.kind === kind);
|
|
653
|
+
const matches = profiles.filter((p) => p.teamId === teamId && p.bundleId === bundleId && p.exp > now && kindOk(p));
|
|
642
654
|
if (matches.length === 0) return null;
|
|
643
655
|
const pinned = cfg.signingProfiles?.[bundleId];
|
|
644
656
|
if (pinned && matches.some((p) => p.name === pinned)) return pinned;
|
|
@@ -678,14 +690,31 @@ function stampManualSigning(outDir: string, cfg: AppwrapConfig, ctx?: { configPa
|
|
|
678
690
|
console.warn(' ⚠ signing: manual but teamId is unset — skipping manual signing.');
|
|
679
691
|
return;
|
|
680
692
|
}
|
|
693
|
+
// Store archives (`appwrap build ios --release`) sign with App Store distribution profiles +
|
|
694
|
+
// "Apple Distribution"; device deploys with development/adhoc profiles + "Apple Development".
|
|
695
|
+
// A signing-DISABLED archive that's re-signed at exportArchive gets its entitlements SYNTHESIZED
|
|
696
|
+
// from the profile (dropping app.entitlements — e.g. declared-age-range); signing the archive
|
|
697
|
+
// itself is what preserves them, so the store lane rides this same manual stamp.
|
|
698
|
+
const kind: ProvKind | 'device' = process.env.APPWRAP_SIGNING_KIND === 'appstore' ? 'appstore' : 'device';
|
|
699
|
+
const identity = kind === 'appstore' ? 'Apple Distribution' : 'Apple Development';
|
|
700
|
+
// Don't pin interactive picks from the store lane — the single signingProfiles map is keyed by
|
|
701
|
+
// bundle id only, so a store pin would clobber the device pin (and vice versa).
|
|
702
|
+
const pinCtx = kind === 'device' ? ctx : undefined;
|
|
681
703
|
const profiles = findProvisioningProfiles();
|
|
682
704
|
let mainProvision: string | null = null;
|
|
683
705
|
|
|
684
706
|
// Main app target → build.xcconfig.
|
|
685
707
|
const xcconfig = join(outDir, 'App_Resources/iOS/build.xcconfig');
|
|
686
708
|
if (existsSync(xcconfig)) {
|
|
687
|
-
const name = selectSigningProfile(profiles, teamId, cfg.id, cfg,
|
|
709
|
+
const name = selectSigningProfile(profiles, teamId, cfg.id, cfg, pinCtx, kind);
|
|
688
710
|
if (!name) {
|
|
711
|
+
// Store lane must NOT fall through to automatic signing: an unsigned archive gets its
|
|
712
|
+
// entitlements re-synthesized from the profile at export, silently dropping any
|
|
713
|
+
// config-stamped entitlement (the declared-age-range failure this lane exists to prevent).
|
|
714
|
+
if (kind === 'appstore') {
|
|
715
|
+
console.error(` ✗ no valid App Store distribution profile for ${cfg.id} (team ${teamId}). Install one (ASC → Profiles) or seed via Xcode, then retry.`);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
689
718
|
console.warn(` ⚠ no valid profile for ${cfg.id} (team ${teamId}) — automatic signing will run. Install one or seed via Xcode.`);
|
|
690
719
|
} else {
|
|
691
720
|
mainProvision = name;
|
|
@@ -696,10 +725,10 @@ function stampManualSigning(outDir: string, cfg: AppwrapConfig, ctx?: { configPa
|
|
|
696
725
|
? s.replace(new RegExp(`^\\s*${key}\\s*=\\s*[^;\\n]*;?`, 'm'), `${key} = ${val};`)
|
|
697
726
|
: s + `\n${key} = ${val};`;
|
|
698
727
|
src = setKey(src, 'CODE_SIGN_STYLE', 'Manual');
|
|
699
|
-
src = setKey(src, 'CODE_SIGN_IDENTITY',
|
|
728
|
+
src = setKey(src, 'CODE_SIGN_IDENTITY', identity);
|
|
700
729
|
src = setKey(src, 'PROVISIONING_PROFILE_SPECIFIER', name);
|
|
701
730
|
writeFileSync(xcconfig, src.endsWith('\n') ? src : src + '\n');
|
|
702
|
-
console.log(` sign ← ${cfg.id} → "${name}" (manual)`);
|
|
731
|
+
console.log(` sign ← ${cfg.id} → "${name}" (manual, ${identity})`);
|
|
703
732
|
}
|
|
704
733
|
}
|
|
705
734
|
|
|
@@ -715,7 +744,7 @@ function stampManualSigning(outDir: string, cfg: AppwrapConfig, ctx?: { configPa
|
|
|
715
744
|
if (!ext.isDirectory()) continue;
|
|
716
745
|
if (!existsSync(join(extRoot, ext.name, 'extension.json'))) continue;
|
|
717
746
|
const extBundle = `${cfg.id}.${ext.name}`;
|
|
718
|
-
const name = selectSigningProfile(profiles, teamId, extBundle, cfg,
|
|
747
|
+
const name = selectSigningProfile(profiles, teamId, extBundle, cfg, pinCtx, kind);
|
|
719
748
|
if (!name) { console.warn(` ⚠ no valid profile for extension ${extBundle} — it may fail to sign.`); continue; }
|
|
720
749
|
extEntries.push({ bundleId: extBundle, profile: name });
|
|
721
750
|
console.log(` sign ← ${extBundle} → "${name}" (manual, via hook)`);
|
|
@@ -725,7 +754,7 @@ function stampManualSigning(outDir: string, cfg: AppwrapConfig, ctx?: { configPa
|
|
|
725
754
|
const hookFile = join(hookDir, 'appwrap-signing.js');
|
|
726
755
|
if (extEntries.length) {
|
|
727
756
|
mkdirSync(hookDir, { recursive: true });
|
|
728
|
-
writeFileSync(hookFile, SIGNING_HOOK(teamId, extEntries));
|
|
757
|
+
writeFileSync(hookFile, SIGNING_HOOK(teamId, extEntries, identity));
|
|
729
758
|
// NS's exportOptions reads extension profiles from this file (extensions/provisioning.json) —
|
|
730
759
|
// it maps each extension bundle id → profile name so `xcodebuild -exportArchive` can build the .ipa.
|
|
731
760
|
writeFileSync(join(extRoot, 'provisioning.json'),
|
|
@@ -751,11 +780,12 @@ function stampManualSigning(outDir: string, cfg: AppwrapConfig, ctx?: { configPa
|
|
|
751
780
|
* Self-contained (no deps), zero-arg (NS DI won't choke). Per extension it upserts DEVELOPMENT_TEAM,
|
|
752
781
|
* PROVISIONING_PROFILE_SPECIFIER, CODE_SIGN_STYLE=Manual, CODE_SIGN_IDENTITY in every buildSettings
|
|
753
782
|
* block whose PRODUCT_BUNDLE_IDENTIFIER matches the extension's bundle id. */
|
|
754
|
-
const SIGNING_HOOK = (team: string, entries: Array<{ bundleId: string; profile: string }
|
|
783
|
+
const SIGNING_HOOK = (team: string, entries: Array<{ bundleId: string; profile: string }>, identity = 'Apple Development') => `// Generated by \`appwrap\` — stamps manual signing onto extension targets in project.pbxproj.
|
|
755
784
|
const fs = require('fs');
|
|
756
785
|
const path = require('path');
|
|
757
786
|
const TEAM = ${JSON.stringify(team)};
|
|
758
787
|
const ENTRIES = ${JSON.stringify(entries)};
|
|
788
|
+
const IDENTITY = ${JSON.stringify(identity)};
|
|
759
789
|
module.exports = function () {
|
|
760
790
|
const iosDir = path.join(__dirname, '..', '..', 'platforms', 'ios');
|
|
761
791
|
if (!fs.existsSync(iosDir)) return;
|
|
@@ -781,7 +811,7 @@ module.exports = function () {
|
|
|
781
811
|
block = block.replace(/^\\s*PROVISIONING_PROFILE\\s*=\\s*[^;\\n]*;\\n?/m, '');
|
|
782
812
|
block = upsert(block, 'CODE_SIGN_STYLE', 'Manual');
|
|
783
813
|
block = upsert(block, 'DEVELOPMENT_TEAM', TEAM);
|
|
784
|
-
block = upsert(block, 'CODE_SIGN_IDENTITY', q(
|
|
814
|
+
block = upsert(block, 'CODE_SIGN_IDENTITY', q(IDENTITY));
|
|
785
815
|
block = upsert(block, 'PROVISIONING_PROFILE_SPECIFIER', q(profile));
|
|
786
816
|
return block;
|
|
787
817
|
});
|
|
@@ -1275,10 +1305,18 @@ function stampVersionManifest(outDir: string, cfg: AppwrapConfig): void {
|
|
|
1275
1305
|
|
|
1276
1306
|
/** Pure-native escape hatch: copy the consumer's overrides dir OVER the generated wrapper, last,
|
|
1277
1307
|
* so it wins. For legacy/custom native code the declarative config can't express. */
|
|
1278
|
-
function applyOverrides(cwd: string, outDir: string, cfg: AppwrapConfig): void {
|
|
1308
|
+
export function applyOverrides(cwd: string, outDir: string, cfg: AppwrapConfig): void {
|
|
1279
1309
|
const dir = resolve(cwd, cfg.overrides ?? 'appwrap.overrides');
|
|
1280
|
-
|
|
1310
|
+
// Files the template still provides must survive an override removal (regenerateCore ran first and
|
|
1311
|
+
// re-copied them) → protect them from the override prune.
|
|
1312
|
+
const templateFiles = existsSync(TEMPLATE_DIR) ? collectRelFiles(TEMPLATE_DIR, templateCopyFilter) : new Set<string>();
|
|
1313
|
+
if (!existsSync(dir)) {
|
|
1314
|
+
// No overrides now: prune anything a PRIOR overrides run left behind (renamed/removed override files).
|
|
1315
|
+
pruneStale(outDir, OVERRIDES_MANIFEST, new Set(), templateFiles);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1281
1318
|
cpSync(dir, outDir, { recursive: true, force: true });
|
|
1319
|
+
pruneStale(outDir, OVERRIDES_MANIFEST, collectRelFiles(dir), templateFiles);
|
|
1282
1320
|
console.log(` over ← ${dir} (native overrides applied)`);
|
|
1283
1321
|
}
|
|
1284
1322
|
|
|
@@ -1422,6 +1460,54 @@ function copyCiTemplates(cwd: string, outDir: string, cfg: AppwrapConfig, force
|
|
|
1422
1460
|
console.log(ci);
|
|
1423
1461
|
}
|
|
1424
1462
|
|
|
1463
|
+
// Manifests of what the two sync-owned sources (runtime template, consumer overrides) generated LAST run.
|
|
1464
|
+
// Used to prune files that were REMOVED/renamed in the source — cpSync(force) overwrites but never deletes,
|
|
1465
|
+
// so without this a relocated source lingers in native/ and gets re-bundled.
|
|
1466
|
+
const TEMPLATE_MANIFEST = '.appwrap-template-manifest.json';
|
|
1467
|
+
const OVERRIDES_MANIFEST = '.appwrap-overrides-manifest.json';
|
|
1468
|
+
|
|
1469
|
+
/** Files under TEMPLATE_DIR to skip when copying/walking it (deps, build output, PWA staging, per-module
|
|
1470
|
+
* native — those are handled selectively elsewhere). Shared by the cpSync filter and the prune walk so
|
|
1471
|
+
* the copied set and the pruned set are defined identically. */
|
|
1472
|
+
const templateCopyFilter = (src: string): boolean =>
|
|
1473
|
+
!/(?:^|\/)(node_modules|platforms|hooks|app\/www|modules-native)(\/|$)/.test(src.slice(TEMPLATE_DIR.length));
|
|
1474
|
+
|
|
1475
|
+
/** File (not dir) paths under `root`, relative to it, skipping entries `accept` rejects. */
|
|
1476
|
+
function collectRelFiles(root: string, accept: (abs: string) => boolean = () => true): Set<string> {
|
|
1477
|
+
const out = new Set<string>();
|
|
1478
|
+
const rec = (dir: string, rel: string): void => {
|
|
1479
|
+
let entries;
|
|
1480
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
1481
|
+
for (const e of entries) {
|
|
1482
|
+
const abs = join(dir, e.name);
|
|
1483
|
+
if (!accept(abs)) continue;
|
|
1484
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
1485
|
+
if (e.isDirectory()) rec(abs, r);
|
|
1486
|
+
else out.add(r);
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
rec(root, '');
|
|
1490
|
+
return out;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/** Prune generated files sync OWNS that vanished from their source. Deletes only paths recorded in the
|
|
1494
|
+
* PRIOR manifest that are absent from the `current` source set — so it never touches user data or any
|
|
1495
|
+
* region absent from a previous run's manifest. `protect` keeps paths another owned source still ships
|
|
1496
|
+
* (e.g. an override was removed but the template still provides that file). */
|
|
1497
|
+
function pruneStale(outDir: string, manifest: string, current: Set<string>, protect?: Set<string>): void {
|
|
1498
|
+
const mf = join(outDir, manifest);
|
|
1499
|
+
let prev: string[] = [];
|
|
1500
|
+
try { prev = JSON.parse(readFileSync(mf, 'utf8')) as string[]; } catch { /* first run — nothing to prune */ }
|
|
1501
|
+
for (const rel of prev) {
|
|
1502
|
+
if (current.has(rel) || protect?.has(rel)) continue;
|
|
1503
|
+
const abs = join(outDir, rel);
|
|
1504
|
+
try {
|
|
1505
|
+
if (existsSync(abs)) { rmSync(abs, { force: true }); console.log(` prune ✕ ${rel} (removed from source)`); }
|
|
1506
|
+
} catch { /* non-fatal */ }
|
|
1507
|
+
}
|
|
1508
|
+
try { writeFileSync(mf, JSON.stringify([...current])); } catch { /* non-fatal */ }
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1425
1511
|
/**
|
|
1426
1512
|
* Reproduce native/ from source — the shared core of `init` and `sync`. Copies the runtime shell
|
|
1427
1513
|
* template, re-stamps EVERY config artifact, and re-copies the built PWA. `native/` is disposable, so a
|
|
@@ -1441,8 +1527,10 @@ function regenerateCore(cwd: string, outDir: string, cfg: AppwrapConfig, opts: {
|
|
|
1441
1527
|
// modules-native/ is copied selectively per active module (copyModuleNativeSrc), not wholesale.
|
|
1442
1528
|
// Match RELATIVE to TEMPLATE_DIR — when installed from npm, TEMPLATE_DIR itself sits under
|
|
1443
1529
|
// node_modules/, so testing the absolute path would wrongly exclude the entire template.
|
|
1444
|
-
filter:
|
|
1530
|
+
filter: templateCopyFilter,
|
|
1445
1531
|
});
|
|
1532
|
+
// Delete template files removed/renamed since the last regenerate (cpSync only overwrites, never deletes).
|
|
1533
|
+
pruneStale(outDir, TEMPLATE_MANIFEST, collectRelFiles(TEMPLATE_DIR, templateCopyFilter));
|
|
1446
1534
|
stampShellConfig(outDir, cfg);
|
|
1447
1535
|
stampNativeScriptConfig(outDir, cfg);
|
|
1448
1536
|
stampIOSDisplayName(outDir, cfg, req);
|
|
@@ -1860,13 +1948,29 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
|
|
|
1860
1948
|
console.error(`✖ ${(e as Error).message}`);
|
|
1861
1949
|
process.exit(1);
|
|
1862
1950
|
}
|
|
1951
|
+
const release = 'release' in flags;
|
|
1952
|
+
// iOS release archive = the STORE artifact. With signing:'manual', stamp App Store distribution
|
|
1953
|
+
// signing (profile kind 'appstore' + "Apple Distribution") so xcodebuild archives a SIGNED binary —
|
|
1954
|
+
// an unsigned archive re-signed at exportArchive gets its entitlements SYNTHESIZED from the
|
|
1955
|
+
// provisioning profile, silently dropping app.entitlements (e.g. declared-age-range).
|
|
1956
|
+
if (platform === 'ios' && release) process.env.APPWRAP_SIGNING_KIND = 'appstore';
|
|
1863
1957
|
// Make sure the wrapper reflects the latest config + PWA before compiling (also validates the config).
|
|
1864
1958
|
await sync(cwd, flags);
|
|
1865
1959
|
|
|
1866
|
-
const release = 'release' in flags;
|
|
1867
1960
|
const args = ['build', platform];
|
|
1868
1961
|
if (release) args.push('--release');
|
|
1869
|
-
if (platform === 'ios' && release)
|
|
1962
|
+
if (platform === 'ios' && release) {
|
|
1963
|
+
args.push('--for-device');
|
|
1964
|
+
// Manual signing: pass the resolved App Store profile so NS emits a manual exportOptions.plist
|
|
1965
|
+
// (app + extensions via extensions/provisioning.json) for `xcodebuild -exportArchive`. The export
|
|
1966
|
+
// method comes from the archive's embedded profile (Distribution → app-store), so this yields a
|
|
1967
|
+
// store .ipa whose entitlements are the archive's own (app.entitlements intact).
|
|
1968
|
+
const sidecar = join(outDir, '.appwrap-signing.json');
|
|
1969
|
+
if (existsSync(sidecar)) {
|
|
1970
|
+
const sc = JSON.parse(readFileSync(sidecar, 'utf8')) as { provision?: string };
|
|
1971
|
+
if (sc.provision) args.push('--provision', sc.provision);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1870
1974
|
if (platform === 'android' && 'aab' in flags) args.push('--aab');
|
|
1871
1975
|
|
|
1872
1976
|
if (platform === 'android' && release) {
|
|
@@ -1891,7 +1995,8 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
|
|
|
1891
1995
|
console.log(`▶ ns ${args.join(' ').replace(/(--key-store-password|--key-store-alias-password) [^ ]*/g, '$1 ****')} (cwd: ${outDir})`);
|
|
1892
1996
|
execFileSync('ns', args, { cwd: outDir, stdio: 'inherit' });
|
|
1893
1997
|
if (platform === 'ios' && release) {
|
|
1894
|
-
|
|
1998
|
+
const ipa = join(outDir, 'platforms/ios/build/Release-iphoneos');
|
|
1999
|
+
console.log(`ℹ Signed store .ipa in ${ipa} — upload via \`bunx @livx.cc/mcp-appstores apple-upload\` or the fastlane release lane (native/fastlane).`);
|
|
1895
2000
|
}
|
|
1896
2001
|
}
|
|
1897
2002
|
|
|
@@ -2176,7 +2281,7 @@ function pinTeamIdToConfig(configPath: string, teamId: string): void {
|
|
|
2176
2281
|
* App_Resources/ is intentionally excluded — sync() rewrites it every run, so its mtime always
|
|
2177
2282
|
* changes and would make the fingerprint permanently stale.
|
|
2178
2283
|
* Collision risk is acceptable — a false "match" just skips a redundant build, not a correctness bug. */
|
|
2179
|
-
function buildFingerprint(cwd: string, cfg: { pwaDist?: string },
|
|
2284
|
+
export function buildFingerprint(cwd: string, cfg: { pwaDist?: string; overrides?: string }, flags: Record<string, string> = {}): string {
|
|
2180
2285
|
const mtime = (p: string): number => {
|
|
2181
2286
|
if (!existsSync(p)) return 0;
|
|
2182
2287
|
try {
|
|
@@ -2190,8 +2295,12 @@ function buildFingerprint(cwd: string, cfg: { pwaDist?: string }, _outDir: strin
|
|
|
2190
2295
|
return s.mtimeMs;
|
|
2191
2296
|
} catch { return 0; }
|
|
2192
2297
|
};
|
|
2298
|
+
// Fingerprint EVERY app source the build actually consumes — not just dist. Missing the overrides dir
|
|
2299
|
+
// (native escape hatch) or a `.js`/`.json`/`--config` config file made edits there produce a stale
|
|
2300
|
+
// "inputs unchanged" skip. Resolve the real config path (ts→js→json / --config) instead of hardcoding.
|
|
2193
2301
|
const distDir = cfg.pwaDist ? resolve(cwd, cfg.pwaDist) : join(cwd, 'dist');
|
|
2194
|
-
const
|
|
2302
|
+
const overridesDir = resolve(cwd, cfg.overrides ?? 'appwrap.overrides');
|
|
2303
|
+
const parts = [mtime(distDir), mtime(overridesDir), mtime(resolveConfigPath(cwd, flags))];
|
|
2195
2304
|
// Simple djb2-style hash — good enough for a build-skip check (not cryptographic).
|
|
2196
2305
|
let h = 5381;
|
|
2197
2306
|
for (const n of parts) h = (((h << 5) + h) ^ (n | 0)) >>> 0;
|
|
@@ -2499,7 +2608,7 @@ async function deployAndroid(cwd: string, flags: Record<string, string>, cfgOver
|
|
|
2499
2608
|
// unchanged since the last build. --force/-f always rebuilds. (Rebuilding the web above usually
|
|
2500
2609
|
// bumps the fingerprint; pair with --no-web-build to actually hit this fast-path.)
|
|
2501
2610
|
const force = 'force' in flags || 'f' in flags;
|
|
2502
|
-
const fp = buildFingerprint(cwd, cfg,
|
|
2611
|
+
const fp = buildFingerprint(cwd, cfg, flags);
|
|
2503
2612
|
const cache = readBuildCache(outDir, 'android');
|
|
2504
2613
|
if (!force && existsSync(apk) && cache?.fingerprint === fp && cache?.artifactPath === apk) {
|
|
2505
2614
|
console.log(`⚡ Skipping build — inputs unchanged since last build (${apk.split('/').pop()})`);
|
|
@@ -2577,7 +2686,7 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
|
|
|
2577
2686
|
// Auto: always checks fingerprint; never skips if sources/deps changed.
|
|
2578
2687
|
const resume = 'resume' in flags || 'r' in flags;
|
|
2579
2688
|
const force = 'force' in flags || 'f' in flags;
|
|
2580
|
-
const fp = buildFingerprint(cwd, cfg,
|
|
2689
|
+
const fp = buildFingerprint(cwd, cfg, flags);
|
|
2581
2690
|
const cache = readBuildCache(outDir, 'ios');
|
|
2582
2691
|
const existingIpa = existsSync(ipaDir)
|
|
2583
2692
|
? readdirSync(ipaDir).find((f) => f.endsWith('.ipa'))
|