apps folders add adds the folder (template note, refusals with reasons); MCP: .mcp.json never spawns a missing plugin path (sh fallback to PATH, npx), mcp/serve skip the self-update, spawned handshake test; store build minimums in one dated table: Play target API 36 and App Store Xcode 26 / SDK 26 blocked in scan, validate, play upload/release, apple upload; pitfalls 81-83

This commit is contained in:
CRTkafa 2026-10-20 12:15:00 +07:00
parent 9805582278
commit 9ab64bcbdf
16 changed files with 323 additions and 14 deletions

View file

@ -8,6 +8,7 @@ import { basename, dirname, extname, join, relative, sep } from 'node:path';
import { resolvePackage } from '../node-packages';
import { isBuildCopyPath, type Manifest, parseManifest } from '../privacy-manifest';
import { type DepRef, type Eco, matchesSdk, SDKS, type SdkDef } from './sdks';
import { exportedIpas, ipaInfo } from '../xcode';
export interface Loc { file: string; line: number }
export interface PlistEntry { key: string; value: unknown; at: Loc }
@ -88,6 +89,8 @@ export interface ProjectFacts {
sources: { path: string; text: string }[];
/** Folders of built web output inside the project (Capacitor webDir): low-trust evidence, see isBuilt. */
builtDirs: string[];
/** Xcode / SDK of the newest IPA in build/ios/export (Info.plist DTXcode, DTSDKName, DTPlatformVersion). */
iosBuild?: { file: string; xcode?: string; sdkName?: string; platformVersion?: string } | null;
/** Files looked at, relative to root (for the report). */
scanned: number;
}
@ -542,7 +545,7 @@ export function detectProject(root: string): ProjectFacts {
for (const x of extra.services) if (!svc.has(key('service', x.name))) android.services.push(x);
}
}
return { root, stacks: [...stacks], ios, android: isAndroid, deps, sdks, plist, plistSources, entitlements, privacyManifests, androidInfo: android, native, remoteStart, sources, builtDirs, scanned: files.length };
return { root, stacks: [...stacks], ios, android: isAndroid, deps, sdks, plist, plistSources, entitlements, privacyManifests, androidInfo: android, native, remoteStart, sources, builtDirs, iosBuild: newestIpaBuild(root), scanned: files.length };
}
/** NDK r27 flag and the linker flag of older NDKs that make native libraries 16 KB aligned. */
@ -640,3 +643,13 @@ export const where = (l: Loc): string => `${l.file}:${l.line}`;
/** Ecosystems in a readable order for the report. */
export const ECO_LABEL: Record<Eco, string> = { cocoapods: 'CocoaPods', spm: 'SwiftPM', carthage: 'Carthage', gradle: 'Gradle', npm: 'npm', pub: 'pub', unity: 'Unity', godot: 'Godot' };
/** The build tools of the newest IPA under build/ios/export, or null (no IPA, or unreadable). */
function newestIpaBuild(root: string): ProjectFacts['iosBuild'] {
try {
const ipa = exportedIpas(join(root, 'build/ios/export'))[0];
if (!ipa) return null;
const i = ipaInfo(ipa);
return { file: relative(root, ipa).split(sep).join('/'), xcode: i.xcode, sdkName: i.sdkName, platformVersion: i.platformVersion };
} catch { return null; }
}

View file

@ -47,6 +47,7 @@ import { playRules } from './play-rules';
import { grep, grepOwn, type Hit, lineOf, type ProjectFacts } from './project';
import { matchesSdk, OBFUSCATORS, REMOTE_CODE, REMOTE_SDKS, type RemoteSdkDef, versionAtLeast } from './sdks';
import { reviewNotes } from '../review-details';
import { appleSdkProblem, PLAY_TARGET_SDK_TABLE } from '../store-minimums';
export type Severity = 'blocker' | 'high' | 'medium' | 'low';
export type ScanStore = 'apple' | 'play';
@ -93,8 +94,10 @@ export interface ScanInput {
export const APPLE_GUIDELINES = 'https://developer.apple.com/app-store/review/guidelines/';
const PLAY = 'https://support.google.com/googleplay/android-developer/answer/';
export const PLAY_POLICY = 'https://play.google/developer-content-policy/';
export const PLAY_TARGET_SDK = { phone: 36, wear: 35, automotive: 35, tv: 34 } as const;
export const PLAY_TARGET_SDK_SINCE = '2026-08-31';
/** The Play target API level in force today (the dated table is cli/lib/store-minimums.ts). */
const PLAY_SDK_ROW = PLAY_TARGET_SDK_TABLE.filter(r => r.since <= new Date().toISOString().slice(0, 10)).at(-1) ?? PLAY_TARGET_SDK_TABLE[0]!;
export const PLAY_TARGET_SDK = PLAY_SDK_ROW.min;
export const PLAY_TARGET_SDK_SINCE = PLAY_SDK_ROW.since;
// ---- code signals ----
/** `re` only on lines that are not import / require / export-from statements (a module path is not evidence). Line
@ -363,6 +366,13 @@ export function scanRules(inp: ScanInput): ScanFinding[] {
}
// ---- App Tracking Transparency ----
// ---- App Store: the Xcode / SDK of the newest local IPA (cli/lib/store-minimums.ts) ----
if (apple && p.iosBuild) {
const why = appleSdkProblem(p.iosBuild);
if (why) add({ rule: 'apple-min-sdk', guideline: 'App Store: minimum Xcode / SDK', store: 'apple', severity: 'blocker', title: why.split(': ')[0]!,
evidence: [{ file: p.iosBuild.file, line: 1, text: [p.iosBuild.xcode && `DTXcode ${p.iosBuild.xcode}`, p.iosBuild.sdkName && `DTSDKName ${p.iosBuild.sdkName}`].filter(Boolean).join(', ') }],
fix: why.slice(why.indexOf(': ') + 2), skill: 'build-and-sign', ref: 'https://developer.apple.com/news/upcoming-requirements/' });
}
if (apple && p.ios) {
const trackers = p.sdks.filter(s => s.sdk.tracking);
const trackEv = sdkHits(s => s.sdk.tracking);
@ -552,7 +562,7 @@ export function scanRules(inp: ScanInput): ScanFinding[] {
const need = PLAY_TARGET_SDK[a.formFactor];
if (a.targetSdk && a.targetSdk.value < need)
add({ rule: 'play-target-sdk', guideline: 'Play: Target API level', store: 'play', severity: 'blocker', title: `targetSdk ${a.targetSdk.value} is below ${need}, which new apps and updates need since ${PLAY_TARGET_SDK_SINCE}`, evidence: [{ ...a.targetSdk.at, text: `targetSdk ${a.targetSdk.value}` }],
fix: `Raise targetSdk (and compileSdk) to ${need}, test the behavior changes of each Android version in between, then upload a new AAB (an extension to 2026-11-01 can be requested in Play Console).`, skill: 'play-first-release',
fix: `Set targetSdkVersion = ${need} (and compileSdkVersion) in ${a.targetSdk.at.file} and rebuild; test the behavior changes of each Android version in between, then upload a new AAB ${PLAY_SDK_ROW.extensionUntil ? ` (an extension to ${PLAY_SDK_ROW.extensionUntil} can be requested in Play Console)` : ''}.`, skill: 'play-first-release',
ref: 'https://developer.android.com/google/play/requirements/target-sdk' });
else if (!a.targetSdk && a.targetSdkFrom)
add({ rule: 'play-target-sdk', guideline: 'Play: Target API level', store: 'play', severity: 'low', title: `targetSdk comes from ${a.targetSdkFrom}: check that the built AAB targets ${need} or higher`, evidence: a.manifests.slice(0, 1).map(f => ({ file: f, line: 1, text: 'AndroidManifest.xml' })),