import { readFile, readdir, stat } from 'node:fs/promises'; import { join, resolve as resolvePath } from 'node:path'; import { pathToFileURL } from 'node:url'; import { prepareBuildArtifactsFromConfigFile } from './context.ts'; const PREPARE_SUBCOMMAND = 'prepare'; const ANDROID_ORIENTATION_RETRY_ATTEMPTS = 4; const ANDROID_ORIENTATION_RETRY_DELAY_MS = 250; export interface PrepareCommandArgs { config: string; platform: string; output: string; projectDir?: string; extensionName?: string; } interface AndroidOrientationFlags { landscapeRight?: boolean; landscapeLeft?: boolean; portrait?: boolean; upsideDown?: boolean; } function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function resolveAndroidScreenOrientationFromFlags(orientation?: AndroidOrientationFlags | null) { if (!orientation) { return null; } const hasPortrait = Boolean(orientation.portrait || orientation.upsideDown); const hasLandscape = Boolean(orientation.landscapeLeft || orientation.landscapeRight); if (hasPortrait && !hasLandscape) { return 'portrait'; } if (hasLandscape && !hasPortrait) { return 'landscape'; } if (hasPortrait && hasLandscape) { return 'fullSensor'; } return null; } async function resolveExpectedAndroidScreenOrientationFromBuilderLog(projectDir?: string) { if (!projectDir) { return null; } const builderLogDir = join(projectDir, 'temp/builder/log'); try { const entries = await readdir(builderLogDir); const files = await Promise.all(entries.map(async (entry) => { const path = join(builderLogDir, entry); const fileStat = await stat(path); return { path, mtimeMs: fileStat.mtimeMs, }; })); const latestFiles = files .filter((entry) => Number.isFinite(entry.mtimeMs)) .sort((left, right) => right.mtimeMs - left.mtimeMs); for (const file of latestFiles) { const content = await readFile(file.path, 'utf8'); const markerIndex = content.indexOf('Start build task, options:'); if (markerIndex < 0) { continue; } const jsonStart = content.indexOf('{', markerIndex); if (jsonStart < 0) { continue; } let jsonEnd = content.indexOf('\n', jsonStart); if (jsonEnd < 0) { jsonEnd = content.length; } const buildOptions = JSON.parse(content.slice(jsonStart, jsonEnd).trim().replace(/\r$/, '')) as { platform?: string; packages?: { android?: { orientation?: AndroidOrientationFlags; }; }; }; if (buildOptions.platform !== 'android') { continue; } const orientation = resolveAndroidScreenOrientationFromFlags(buildOptions.packages?.android?.orientation); if (orientation) { return orientation; } } } catch { return null; } return null; } async function resolveExpectedAndroidScreenOrientation(outputDir: string, projectDir?: string) { const orientationFromBuilderLog = await resolveExpectedAndroidScreenOrientationFromBuilderLog(projectDir); if (orientationFromBuilderLog) { return orientationFromBuilderLog; } const compileConfigPath = join(outputDir, 'cocos.compile.config.json'); try { const compileConfig = JSON.parse(await readFile(compileConfigPath, 'utf8')) as { packages?: { android?: { orientation?: AndroidOrientationFlags; }; }; }; return resolveAndroidScreenOrientationFromFlags(compileConfig.packages?.android?.orientation); } catch { return null; } } async function resolveCurrentAndroidManifestScreenOrientation(outputDir: string) { const androidManifestPath = join(outputDir, 'native/engine/android/app/AndroidManifest.xml'); try { const manifest = await readFile(androidManifestPath, 'utf8'); const activityTagMatch = manifest.match(/]*android:name="com\.cocos\.game\.AppActivity"[^>]*>/m); if (!activityTagMatch) { return null; } const orientationMatch = activityTagMatch[0].match(/android:screenOrientation="([^"]+)"/); return orientationMatch?.[1] ?? null; } catch { return null; } } export async function stabilizePreparedAndroidManifestOrientation(options: { outputDir: string; projectDir?: string; prepare: () => Promise>>; maxAttempts?: number; retryDelayMs?: number; }) { const expectedOrientation = await resolveExpectedAndroidScreenOrientation(options.outputDir, options.projectDir); if (!expectedOrientation) { return null; } const maxAttempts = options.maxAttempts ?? ANDROID_ORIENTATION_RETRY_ATTEMPTS; const retryDelayMs = options.retryDelayMs ?? ANDROID_ORIENTATION_RETRY_DELAY_MS; let lastResult: Awaited> | null = null; for (let attempt = 0; attempt < maxAttempts; attempt += 1) { const currentOrientation = await resolveCurrentAndroidManifestScreenOrientation(options.outputDir); if (currentOrientation !== expectedOrientation) { lastResult = await options.prepare(); } await sleep(retryDelayMs); const settledOrientation = await resolveCurrentAndroidManifestScreenOrientation(options.outputDir); if (settledOrientation === expectedOrientation) { return lastResult; } // The orientation was overwritten after prepare() returned (likely by Cocos Creator's own // post-build file writes). Write the correct orientation again immediately so that this // attempt always ends with the manifest in the expected state, regardless of whether the // next iteration or the loop exit comes next. lastResult = await options.prepare(); } return lastResult; } export function parsePrepareCommandArgs(argv: string[]): PrepareCommandArgs { const args = [...argv]; if (args[0] === PREPARE_SUBCOMMAND) { args.shift(); } const parsed: Partial = {}; while (args.length > 0) { const current = args.shift(); switch (current) { case '--config': parsed.config = args.shift(); break; case '--platform': parsed.platform = args.shift(); break; case '--output': parsed.output = args.shift(); break; case '--project-dir': parsed.projectDir = args.shift(); break; case '--extension-name': parsed.extensionName = args.shift(); break; default: throw new Error(`Unknown prepare command argument: ${current}`); } } if (!parsed.config || !parsed.platform || !parsed.output) { throw new Error('Missing required arguments: --config --platform --output'); } return parsed as PrepareCommandArgs; } export async function runPrepareCommand(args: PrepareCommandArgs) { const prepare = async () => await prepareBuildArtifactsFromConfigFile({ adsConfigPath: args.config, cocosBuildOptions: { platform: args.platform, }, outputDir: args.output, projectDir: args.projectDir, extensionName: args.extensionName, }); let result = await prepare(); if (args.platform === 'android') { const retryResult = await stabilizePreparedAndroidManifestOrientation({ outputDir: resolvePath(args.output), projectDir: args.projectDir ? resolvePath(args.projectDir) : undefined, prepare, }); if (retryResult) { result = retryResult; } } return result; } export async function main(argv = process.argv.slice(2)) { const args = parsePrepareCommandArgs(argv); const result = await runPrepareCommand(args); process.stdout.write(`${JSON.stringify({ targetPlatform: result.targetPlatform, provider: result.selection.provider, generatedFiles: result.generatedFiles.length, extensionFiles: result.extensionFiles.length, nativeTemplateFiles: result.nativeTemplateFiles.length, nativeProviderFiles: result.nativeProviderFiles.length, patchedFiles: result.nativePatchResult.patchedFiles.length, skippedPatchFiles: result.nativePatchResult.skippedFiles.length, patchWarnings: result.nativePatchResult.warnings, iosPatchRoots: result.nativePatchResult.iosPatchRoots, iosIntegrationStatus: result.iosIntegrationStatus ?? null, iosLocalPodDirectory: result.iosLocalPodDirectory ?? null, iosLocalPodspecPath: result.iosLocalPodspecPath ?? null, podfilePath: result.podfilePath ?? null, iosManualStep: result.iosManualStep ?? null, iosWorkspaceHint: result.iosWorkspaceHint ?? null, }, null, 2)}\n`); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { await main(process.argv.slice(2)); }