Files
maka/scripts/verify-windows-x64.mjs
agent-radar 7be535059e
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 19:05:14 +08:00

345 lines
13 KiB
JavaScript

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createHash } from 'node:crypto';
import { access, mkdtemp, open, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { readProductManifestIdentity } from './product-release-identity.mjs';
import { assertPackagedUpdateConfiguration } from './desktop-update-contract.mjs';
import { resolveDesktopBuildVersion, resolveRuntimeHostSetupPackage } from './desktop-nightly.mjs';
import {
assertMissing,
assertPackagedDependencyClosure,
assertPackagedResources,
isolatedUserEnv,
makePtyProbe,
runCommand,
sha256File,
smokePackagedRenderer,
} from './verify-packaged-app.mjs';
const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const executableName = 'Maka.exe';
const amd64Machine = 0x8664;
const temporaryCleanupRetries = 20;
const temporaryCleanupRetryDelayMs = 250;
// conpty echoes the command and terminates lines with CRLF, so the probe keeps
// matching on a substring rather than the whole output.
function runCommandFromRepo(command, args, options = {}) {
return runCommand(command, args, { cwd: repoRoot, ...options });
}
// The release workflow shows only this script's output, so each stage announces
// itself: an unfinished stage is the one that hung.
function step(message) {
console.log(`[verify-windows] ${message}`);
}
function runPowerShell(run, script) {
return run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script]);
}
// A single-quoted PowerShell string is literal — a double-quoted one would
// expand `$` in a path we did not choose.
export function powerShellLiteral(value) {
return `'${value.replace(/'/g, "''")}'`;
}
// electron-builder writes the Windows product version resource in the four-part
// form Windows wants (app-builder-lib `AppInfo.getVersionInWeirdWindowsForm`),
// so 0.1.5 ships as 0.1.5.0 and the release version is its first three parts.
// The fourth part is a build number, which is 0 unless one is configured.
export function assertWindowsProductVersion(productVersion, expectedVersion) {
const [expected] = expectedVersion.split('-');
const parts = productVersion.trim().split('.');
if (parts.length !== 4 || parts.slice(0, 3).join('.') !== expected || !/^\d+$/.test(parts[3])) {
throw new Error(
`Expected app version ${expected}.<build>, found ${productVersion.trim() || '<none>'}.`,
);
}
}
export async function verifyPackagedWindowsSandboxLifecycle(
sandboxExecutable,
{ run = runCommandFromRepo } = {},
) {
for (const script of ['appcontainer-smoke.ps1', 'acl-recovery-smoke.ps1']) {
await run('pwsh', [
'-NoProfile',
'-File',
join(repoRoot, 'experiments', 'windows-sandbox', script),
'-LauncherPath',
sandboxExecutable,
]);
}
}
// The Windows build is unsigned, so the only architecture evidence in the
// artifact is the PE header of the executable itself.
export async function readPeMachine(path) {
const file = await open(path, 'r');
try {
const header = Buffer.alloc(4);
const { bytesRead } = await file.read(header, 0, 4, 0x3c);
if (bytesRead !== 4) {
throw new Error(`${path} is too small to be a PE image.`);
}
const peOffset = header.readUInt32LE(0);
const signature = Buffer.alloc(6);
const peRead = await file.read(signature, 0, 6, peOffset);
if (peRead.bytesRead !== 6 || signature.toString('latin1', 0, 4) !== 'PE\0\0') {
throw new Error(`${path} is not a PE image.`);
}
return signature.readUInt16LE(4);
} finally {
await file.close();
}
}
export async function verifyPackagedWindowsApp(
appDirectory,
{
run = runCommandFromRepo,
requirePath = access,
forbidPath = assertMissing,
readMachine = readPeMachine,
smokeRenderer = smokePackagedRenderer,
workingDirectory = appDirectory,
expectedVersion,
artifactContract = 'current',
environment = process.env,
} = {},
) {
if (artifactContract !== 'current' && artifactContract !== 'upgrade-baseline') {
throw new Error(`Unknown packaged Windows artifact contract: ${artifactContract}`);
}
const requiresCurrentContract = artifactContract === 'current';
const product = await readProductManifestIdentity();
const resources = join(appDirectory, 'resources');
const executable = join(appDirectory, executableName);
const appAsar = join(resources, 'app.asar');
const sandboxExecutable = join(resources, 'windows-sandbox', 'maka-windows-sandbox.exe');
step('checking packaged resources');
await requirePath(executable);
await assertPackagedResources(resources, {
requirePath,
forbidPath,
requireWindowsSandbox: requiresCurrentContract,
requireDisclaimer: requiresCurrentContract,
requireCanonicalIcon: requiresCurrentContract,
requireAppIconCatalog: requiresCurrentContract,
requireDirectPeerArtifact: requiresCurrentContract,
});
// The upgrade baseline is a build that shipped on its own channel, from its
// own commit: its update feed and dependency closure are the ones that were
// right for it, not the ones this checkout expects.
if (requiresCurrentContract) {
await assertPackagedUpdateConfiguration(resources, {
channel: environment.MAKA_DESKTOP_NIGHTLY_VERSION ? 'nightly' : 'release',
});
await assertPackagedDependencyClosure(resources);
}
step('reading the executable architecture');
const machine = await readMachine(executable);
if (machine !== amd64Machine) {
throw new Error(`${executableName} must be x64, found PE machine 0x${machine.toString(16)}.`);
}
if (requiresCurrentContract) {
step('smoking the packaged Windows sandbox');
const sandboxMachine = await readMachine(sandboxExecutable);
if (sandboxMachine !== amd64Machine) {
throw new Error(
`maka-windows-sandbox.exe must be x64, found PE machine 0x${sandboxMachine.toString(16)}.`,
);
}
const sandboxLaunch = {
version: 1,
requestId: 'packaged-sandbox-launch',
executable: sandboxExecutable,
arguments: ['--self-probe'],
cwd: dirname(sandboxExecutable),
readRoots: [],
writeRoots: [],
exactReadRoots: [],
exactWriteRoots: [],
network: 'restricted',
environment: {},
};
const sandboxManifest = join(workingDirectory, 'packaged-sandbox-manifest.json');
await writeFile(
sandboxManifest,
JSON.stringify({
version: 1,
requestId: 'packaged-sandbox',
clientPid: 0,
clientNonce: '0123456789abcdef0123456789abcdef',
profileDigest: createHash('sha256').update(JSON.stringify(sandboxLaunch)).digest('hex'),
launch: sandboxLaunch,
}),
'utf8',
);
const sandboxProbe = await run(sandboxExecutable, ['--broker-local', sandboxManifest]);
// The broker relays the child's stdout as the worker response channel and
// keeps its own boundary diagnostics on stderr, so the channel split itself
// is part of what this probe verifies: the child's --self-probe JSON must
// arrive on stdout and the broker's atomic-job diagnostic on stderr.
if (
!sandboxProbe.stdout.includes('"appContainer":true') ||
!sandboxProbe.stdout.includes('"inJob":true')
) {
throw new Error(
`Packaged Windows sandbox child probe did not reach stdout: ${sandboxProbe.stdout}`,
);
}
if (!sandboxProbe.stderr.includes('"atomicJob":true')) {
throw new Error(
`Packaged Windows sandbox broker diagnostic did not reach stderr: ${sandboxProbe.stderr}`,
);
}
await assertMissing(sandboxManifest);
step('relaying command stdio through the packaged sandbox');
await run('pwsh', [
'-NoProfile',
'-File',
join(repoRoot, 'experiments', 'windows-sandbox', 'stdio-relay-smoke.ps1'),
'-LauncherPath',
sandboxExecutable,
]);
step('verifying packaged sandbox lifecycle recovery');
await verifyPackagedWindowsSandboxLifecycle(sandboxExecutable, { run });
step('running real filesystem-worker operations through the packaged app');
// The evidence executes the packaged artifacts themselves: the packaged
// broker, the packaged Electron executable as the worker runtime and the
// packaged worker bundle under resources — not the repository dist worker
// on a copied node.exe.
const { verifyWindowsSandboxWorkerE2E } = await import('./verify-windows-sandbox-e2e.mjs');
await verifyWindowsSandboxWorkerE2E(appDirectory);
}
step('reading the product version resource');
const { stdout } = await runPowerShell(
run,
`(Get-Item -LiteralPath ${powerShellLiteral(executable)}).VersionInfo.ProductVersion`,
);
assertWindowsProductVersion(
stdout,
expectedVersion ?? resolveDesktopBuildVersion(product.version, environment),
);
step('smoking node-pty through conpty');
const ptyProbe = makePtyProbe(
process.env.ComSpec || 'cmd.exe',
['/c', 'echo', 'maka-node-pty-ok'],
requiresCurrentContract
? resolveRuntimeHostSetupPackage(product.version, environment)
: undefined,
);
await run(executable, ['-e', ptyProbe, join(appAsar, 'package.json')], {
env: {
ELECTRON_RUN_AS_NODE: '1',
...isolatedUserEnv(join(workingDirectory, 'pty-home')),
},
timeoutMs: 60_000,
});
step('smoking the packaged renderer');
await smokeRenderer(executable, {
workingDirectory,
verifyMaximizeRestore: requiresCurrentContract,
});
step('packaged app verified');
}
// Neither release artifact is inspected as an artifact: the NSIS installer has
// no readable app structure, and the ZIP is an archive of the win-unpacked
// directory electron-builder just produced. That directory is the app, so it is
// what gets verified — unpacking the ZIP would only rebuild a copy of it. The
// artifacts themselves are pinned by checksum, and installing the .exe is a
// checklist step. (macOS mounts its DMG instead because notarizing and stapling
// rewrite the DMG after packaging, so only the final artifact can be trusted.)
export async function verifyWindowsX64Release(
inputPath,
{ platform = process.platform, verifyApp = verifyPackagedWindowsApp, checksum = sha256File } = {},
) {
if (platform !== 'win32') {
throw new Error('Windows release verification requires Windows.');
}
if (!inputPath) {
throw new Error('Usage: npm run verify:windows-x64 -- <path-to-exe>');
}
const exePath = resolve(inputPath);
if (!exePath.endsWith('.exe')) {
throw new Error(`Expected the NSIS installer .exe, found ${basename(exePath)}.`);
}
const zipPath = `${exePath.slice(0, -'.exe'.length)}.zip`;
const unpackedDirectory = join(dirname(exePath), 'win-unpacked');
await access(exePath);
await access(zipPath);
await access(unpackedDirectory);
// The smokes write into their working directory, which therefore must not be
// the release directory the artifacts live in.
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'maka-release-verify-'));
try {
await verifyApp(unpackedDirectory, { workingDirectory: temporaryDirectory });
step('checksumming the release artifacts');
const checksums = [];
for (const path of [exePath, zipPath]) {
const sha256 = await checksum(path);
const checksumPath = `${path}.sha256`;
await writeFile(checksumPath, `${sha256} ${basename(path)}\n`, 'utf8');
checksums.push({ path, checksumPath, sha256 });
}
return { exePath, zipPath, unpackedDirectory, checksums };
} finally {
// The Runtime Host intentionally outlives its last Desktop client for its
// idle grace period. On Windows, SQLite keeps the temporary workspace files
// locked until that Host exits. fs.rm retries only the transient filesystem
// errors for recursive removal, so cleanup follows the actual lock lifetime
// without changing the production continuity policy.
await rm(temporaryDirectory, {
recursive: true,
force: true,
maxRetries: temporaryCleanupRetries,
retryDelay: temporaryCleanupRetryDelayMs,
});
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const result = await verifyWindowsX64Release(process.argv[2]);
console.log(`Verified ${result.exePath}`);
for (const { path, sha256 } of result.checksums) {
console.log(`SHA-256 ${sha256} ${basename(path)}`);
}
}