AGC 随包 Node 支持 universal 双架构运行时

- stage-node-runtime 新增 targetRuntimes:universal 展开为 aarch64/x86_64 两份单架构运行时
- 新增 runtimeSourceForTarget:宿主架构取当前构建 Node,其它架构必须由 AGC_NODE_RUNTIME_<PLATFORM>_<ARCH>_PATH 显式提供,缺失即失败关闭
- 把 stageNodeRuntime 拆成 inspectRuntimeSource(只读校验)+ writeRuntimeBundle(落盘),universal 在写出任何目录前完成全部来源校验与版本一致性核对
- stageNodeRuntimeForTarget 单架构仍写扁平 node-runtime/,universal 按架构写 node-runtime/<platform>-<arch>/
- build-release 默认按发布目标 stage,universal 构建不再只带宿主架构的运行时
- environment_check resolve_at 在扁平目录之后按当前运行架构查找 <platform>-<arch> 子目录,两份候选都要过清单校验,存在但损坏则失败关闭
- 覆盖双架构 staging、缺非宿主运行时、版本不一致、按运行架构解析等用例
This commit is contained in:
2026-09-21 11:57:43 +08:00
parent 9f15c1c572
commit 6a0b75779b
5 changed files with 491 additions and 44 deletions
@@ -13,7 +13,7 @@ import {
defaultEditorFeatures,
withDefaultCargoFeatures,
} from './cargo-features.mjs';
import { stageNodeRuntime } from './stage-node-runtime.mjs';
import { stageNodeRuntimeForTarget } from './stage-node-runtime.mjs';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
// 提交摘要里的 pathspec 与 `git log` 都以仓库根为基准,不能在应用目录里执行。
@@ -459,7 +459,8 @@ function writeChannelConfigFile(channel, target, includeNodeRuntime = false) {
export function runTauriBuild(
args = [],
context = resolveReleaseContext(args),
{ spawn = spawnSync, stageRuntime = stageNodeRuntime } = {},
// 默认按发布目标 stage:universal 需要两份架构运行时,单架构目标行为不变。
{ spawn = spawnSync, stageRuntime = stageNodeRuntimeForTarget } = {},
) {
if (
explicitBuildTarget(args) &&
@@ -76,6 +76,33 @@ function run(command, args) {
return result;
}
/**
* 单独执行随包 Node 分片:分片架构与宿主一致时直接跑,不一致时用 `arch` 强制
* arm64 机器上的 x86_64 分片依赖 Rosetta,与 `.app` 双架构 smoke 的前提相同)。
*/
function runNodeSlice(directory, args) {
const native = process.arch === 'arm64' ? 'arm64' : 'x86_64';
const binary = path.join(directory, 'node');
const [command, argv] =
architecture === native
? [binary, args]
: ['/usr/bin/arch', [`-${architecture}`, binary, ...args]];
const result = spawnSync(command, argv, {
cwd: root,
env,
encoding: 'utf8',
timeout: 120_000,
maxBuffer: 1024 * 1024,
});
assert.ifError(result.error);
assert.equal(
result.status,
0,
`${directory} 随包 Node 执行失败:${result.stderr || result.stdout}`,
);
return result.stdout.trim();
}
/**
* APFS 上优先用 `ditto --clone`:整包按区块克隆,秒级完成且几乎不占额外空间。
* 跨卷或非 APFS 时回退到真实复制;两种路径都必须产出可独立改动的副本,
@@ -248,37 +275,84 @@ try {
}
}
assert.ok(fs.existsSync(path.join(bundle, 'NOTICE.md')));
// 随包 Node:单架构构建是扁平目录,universal 构建把两套架构运行时并列放进
// `game-runtime/node/<platform>-<arch>/`(与 Codex 侧车同形态,由 Rust 侧按运行架构选择)。
// 两套清单在本函数里都做完整性与架构校验;只执行与本次 smoke 架构一致的那一份,
// 另一份由另一次架构的 smoke 覆盖(build-macos-ci.mjs 会对 arm64 / x86_64 各跑一次)。
const nodeRoot = path.join(resources, 'game-runtime/node');
const nodeManifest = JSON.parse(
fs.readFileSync(path.join(nodeRoot, 'manifest.json'), 'utf8'),
);
assert.equal(nodeManifest.schemaVersion, 'agc-node-runtime.v1');
assert.equal(nodeManifest.platform, 'darwin');
assert.equal(nodeManifest.arch, process.arch);
const runtimeFiles = fs
.readdirSync(nodeRoot, { recursive: true })
.filter(
(file) =>
fs.statSync(path.join(nodeRoot, file)).isFile() &&
file !== 'manifest.json',
const nodeSlices = requireUniversal
? ['darwin-arm64', 'darwin-x64'].map((platform) => ({
platform,
directory: path.join(nodeRoot, platform),
}))
: [{ platform: null, directory: nodeRoot }];
for (const slice of nodeSlices) {
const { directory } = slice;
const nodeManifest = JSON.parse(
fs.readFileSync(path.join(directory, 'manifest.json'), 'utf8'),
);
assert.deepEqual(runtimeFiles.sort(), Object.keys(nodeManifest.files).sort());
for (const [file, digest] of Object.entries(nodeManifest.files)) {
assert.equal(await hashFile(path.join(nodeRoot, file)), digest, file);
assert.equal(nodeManifest.schemaVersion, 'agc-node-runtime.v1');
assert.equal(nodeManifest.platform, 'darwin');
if (slice.platform) {
// 目录名与清单架构必须一致:错位会让用户拿到跑不起来的运行时。
assert.equal(`darwin-${nodeManifest.arch}`, slice.platform);
assert.ok(
/^(arm64|x64)$/u.test(nodeManifest.arch),
`未知运行架构:${nodeManifest.arch}`,
);
} else {
assert.equal(nodeManifest.arch, process.arch);
}
const runtimeFiles = fs
.readdirSync(directory, { recursive: true })
.filter(
(file) =>
fs.statSync(path.join(directory, file)).isFile() &&
file !== 'manifest.json',
);
assert.deepEqual(
runtimeFiles.sort(),
Object.keys(nodeManifest.files).sort(),
slice.platform ?? 'flat',
);
for (const [file, digest] of Object.entries(nodeManifest.files)) {
assert.equal(await hashFile(path.join(directory, file)), digest, file);
}
assert.ok(nodeManifest.files['NODE-LICENSE']);
assert.ok(nodeManifest.files['node_modules/npm/LICENSE']);
fs.accessSync(path.join(directory, 'node'), fs.constants.X_OK);
// 二进制本身必须是本分片的单一架构:官方发行版不做 universal,lipo 能直接证明。
const sliceArchitecture = spawnSync(
'/usr/bin/lipo',
['-archs', path.join(directory, 'node')],
{ encoding: 'utf8' },
);
assert.equal(sliceArchitecture.status, 0, 'lipo -archs node');
assert.equal(
sliceArchitecture.stdout.trim(),
nodeManifest.arch === 'x64' ? 'x86_64' : 'arm64',
'随包 Node 的二进制架构必须等于清单架构',
);
// 只有与本次 smoke 架构一致的分片才执行;另一架构留给对应的那次 smoke。
if (
!requireUniversal ||
nodeManifest.arch === (architecture === 'arm64' ? 'arm64' : 'x64')
) {
assert.equal(
runNodeSlice(directory, ['--version']),
nodeManifest.nodeVersion,
`${slice.platform ?? 'flat'} node --version`,
);
assert.equal(
runNodeSlice(directory, [
path.join(directory, 'node_modules/npm/bin/npm-cli.js'),
'--version',
]),
nodeManifest.npmVersion,
`${slice.platform ?? 'flat'} npm --version`,
);
}
}
assert.ok(nodeManifest.files['NODE-LICENSE']);
assert.ok(nodeManifest.files['node_modules/npm/LICENSE']);
assert.equal(
run(path.join(nodeRoot, 'node'), ['--version']).stdout.trim(),
nodeManifest.nodeVersion,
);
assert.equal(
run(path.join(nodeRoot, 'node'), [
path.join(nodeRoot, 'node_modules/npm/bin/npm-cli.js'),
'--version',
]).stdout.trim(),
nodeManifest.npmVersion,
);
const plugin = path.join(resources, 'plugins/agc-cocos-editor');
for (const file of [
'plugin.json',
@@ -294,8 +368,13 @@ try {
/(^|\/)(\.env[^/]*|auth\.json|target|\.git)(\/|$)|\.(exe|dll)$/.test(
file,
) ||
// 只有随包 Node 自带的 npm 允许出现 node_modules;两种布局都要放行:
// 单架构的 `game-runtime/node/node_modules/npm` 与 universal 的
// `game-runtime/node/<platform>-<arch>/node_modules/npm`。
(/(^|\/)node_modules(\/|$)/.test(file) &&
!file.startsWith('game-runtime/node/node_modules/npm')),
!/^game-runtime\/node\/((darwin-(arm64|x64))\/)?node_modules\/npm(\/|$)/u.test(
file,
)),
),
);
assert.equal(run(executable, ['--version']).stdout.trim(), manifest.version);
@@ -6,6 +6,13 @@ import { fileURLToPath } from 'node:url';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
export const nodeRuntimeSchema = 'agc-node-runtime.v1';
// 单架构目标写扁平目录;universal 在此目录下按架构分目录,见 stageNodeRuntimeForTarget。
const defaultRuntimeDestination = path.join(
appRoot,
'src-tauri',
'resources',
'node-runtime',
);
export function readInstalledNodeLicense(
version,
@@ -66,10 +73,116 @@ export function targetRuntime(target) {
'x86_64-apple-darwin': ['darwin', 'x64'],
};
const value = targets[target];
if (!value) throw new Error(`Node 运行时不支持发布目标:${target}`);
// universal 不是单份运行时能表达的目标:它必须按架构展开成两份,见 targetRuntimes。
if (!value)
throw new Error(
target === 'universal-apple-darwin'
? 'Node 运行时不能直接按 universal-apple-darwin 制作:双架构请用 stageNodeRuntimeForTarget 按架构展开'
: `Node 运行时不支持发布目标:${target}`,
);
return { platform: value[0], arch: value[1] };
}
/**
* 发布目标需要的随包运行时列表:universal 需要两份单架构运行时,
* 其余目标仍然是一份(保持既有扁平布局与行为)。
*/
export function targetRuntimes(target) {
if (target === 'universal-apple-darwin')
return ['aarch64-apple-darwin', 'x86_64-apple-darwin'];
return [target];
}
/**
* 按架构选择 staging 输入。
*
* 契约(见实施计划):发布包只从**本机已安装且与目标平台/架构一致**的工具链取材,
* 不得使用项目内或相对 PATH 的伪造运行时。宿主架构直接复用当前 Node;
* 其它架构必须由构建节点显式提供,缺失即失败关闭——不静默跳过、不回退系统 Node。
*/
export function runtimeSourceForTarget(
archTarget,
{
env = process.env,
nodePath,
npmCli,
licensePath,
hostNodePath = process.execPath,
hostNpmCli = process.env.npm_execpath,
hostLicensePath = env.AGC_NODE_LICENSE_PATH,
hostPlatform = process.platform,
hostArch = process.arch,
} = {},
) {
const native = targetRuntime(archTarget);
if (native.platform === hostPlatform && native.arch === hostArch) {
return {
nodePath: nodePath ?? hostNodePath,
npmCli: npmCli ?? hostNpmCli,
licensePath: licensePath ?? hostLicensePath,
};
}
const key = `${native.platform}_${native.arch}`.toUpperCase();
const configured = env[`AGC_NODE_RUNTIME_${key}_PATH`]?.trim();
if (!configured) {
throw new Error(
`缺少 ${native.platform}/${native.arch} 的 Node 运行时:该架构不是构建宿主,` +
`请先在本机安装同架构 Node,再用 AGC_NODE_RUNTIME_${key}_PATH 指向其 bin/node`,
);
}
return {
nodePath: configured,
npmCli: undefined,
licensePath: env[`AGC_NODE_RUNTIME_${key}_LICENSE_PATH`]?.trim(),
};
}
/**
* 按发布目标 stage 运行时资源。
*
* 单架构目标沿用既有扁平目录(`node-runtime/`),universal 目标写进
* `node-runtime/<platform>-<arch>/`——与捆绑 Codex 的分架构目录同一形态,
* 由 Rust 侧按当前运行架构选择;构建期资源映射仍是整目录映射,无需按架构分叉。
*
* 先解析并校验**全部**来源,再逐个落盘:非宿主架构的运行时缺失、平台/架构不符、
* 两套版本不一致,都在写出任何运行时目录之前失败,不留下半套资源冒充发布内容。
*/
export function stageNodeRuntimeForTarget(
target,
{ destination = defaultRuntimeDestination, ...options } = {},
) {
const targets = targetRuntimes(target);
const plans = targets.map((archTarget) => {
const native = targetRuntime(archTarget);
return {
destination:
targets.length === 1
? destination
: path.join(destination, `${native.platform}-${native.arch}`),
inspected: inspectRuntimeSource(archTarget, {
...options,
...runtimeSourceForTarget(archTarget, options),
}),
};
});
// 两个切片必须是同一套 Node:版本不一致意味着其中一份被换过,
// 用户在不同架构上会拿到行为不同的工具链。
const identities = new Set(
plans.map(
({ inspected }) =>
`${inspected.info.version}|${inspected.npmPackage.version}`,
),
);
if (identities.size !== 1) {
throw new Error(
`多架构 Node 运行时版本不一致:${[...identities].join(' / ')}`,
);
}
return plans.map(({ inspected, destination }) =>
writeRuntimeBundle(inspected, { destination }),
);
}
function inside(root, file) {
const relative = path.relative(root, file);
return (
@@ -212,13 +325,19 @@ export function assertPortableMacNode(output) {
}
}
export function stageNodeRuntime(
/**
* 只读校验一份运行时来源,返回 staging 需要的全部事实。
*
* 与写盘分离的原因:多架构发布必须能在写出任何文件之前发现「另一份来源缺失或
* 与目标不符」。校验口径保持原样——平台/架构必须等于目标、macOS 二进制只能链接
* 系统动态库、npm 的身份与实际版本必须一致、许可必须来自发行版本体。
*/
export function inspectRuntimeSource(
target,
{
nodePath = process.execPath,
npmCli = process.env.npm_execpath,
licensePath = process.env.AGC_NODE_LICENSE_PATH,
destination = path.join(appRoot, 'src-tauri', 'resources', 'node-runtime'),
execute = execFileSync,
installedLicense = readInstalledNodeLicense,
} = {},
@@ -309,6 +428,14 @@ export function stageNodeRuntime(
throw new Error('Node LICENSE 不包含发行许可');
if (!fs.statSync(path.join(npmRoot, 'LICENSE')).isFile())
throw new Error('npm 缺少 LICENSE');
return { native, node, npmRoot, npmPackage, info, license, licenseName };
}
/** 把已校验的来源写成一份发布资源,返回清单。 */
function writeRuntimeBundle(
{ native, node, npmRoot, npmPackage, info, license, licenseName },
{ destination },
) {
// 临时同级目录完成后才替换资源;不污染 Node 安装或项目工作区。
const requestedDestination = path.resolve(destination);
if (requestedDestination === path.dirname(requestedDestination))
@@ -394,3 +521,26 @@ export function stageNodeRuntime(
cleanupStaging(staging, parent, stagingPrefix, stagingIdentity);
}
}
export function stageNodeRuntime(
target,
{
nodePath = process.execPath,
npmCli = process.env.npm_execpath,
licensePath = process.env.AGC_NODE_LICENSE_PATH,
destination = defaultRuntimeDestination,
execute = execFileSync,
installedLicense = readInstalledNodeLicense,
} = {},
) {
return writeRuntimeBundle(
inspectRuntimeSource(target, {
nodePath,
npmCli,
licensePath,
execute,
installedLicense,
}),
{ destination },
);
}
@@ -9,7 +9,9 @@ import {
assertPortableMacNode,
readInstalledNodeLicense,
stageNodeRuntime,
stageNodeRuntimeForTarget,
targetRuntime,
targetRuntimes,
} from './stage-node-runtime.mjs';
function fixture(run) {
@@ -281,7 +283,11 @@ test('native target and macOS dynamic dependency policy reject nonportable Node'
platform: 'darwin',
arch: 'arm64',
});
assert.throws(() => targetRuntime('universal-apple-darwin'), /不支持/u);
// universal 不是单份运行时目标:必须报出「按架构展开」而不是笼统的「不支持」。
assert.throws(
() => targetRuntime('universal-apple-darwin'),
/stageNodeRuntimeForTarget/u,
);
assertPortableMacNode(
'/node:\n\t/usr/lib/libSystem.B.dylib (compatibility version 1)\n',
);
@@ -293,3 +299,146 @@ test('native target and macOS dynamic dependency policy reject nonportable Node'
/非系统动态库/u,
);
});
// 双架构夹具:每个架构一份来源目录,execute 按被查询的二进制回报对应架构。
function universalFixture(run) {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'agc-node-universal-test-'),
);
const sourceFor = (arch, nodeVersion) => {
const dir = path.join(root, `source-${arch}`);
const npm = path.join(dir, 'node_modules/npm');
fs.mkdirSync(path.join(npm, 'bin'), { recursive: true });
fs.writeFileSync(path.join(dir, 'node'), `node-${arch}`);
fs.writeFileSync(
path.join(dir, 'LICENSE'),
'Node.js\nPermission is hereby granted',
);
fs.writeFileSync(path.join(npm, 'LICENSE'), 'npm distribution license');
fs.writeFileSync(
path.join(npm, 'package.json'),
JSON.stringify({ name: 'npm', version: '11.0.0' }),
);
for (const name of ['npm', 'npx'])
fs.writeFileSync(path.join(npm, `bin/${name}-cli.js`), `// ${arch}`);
return {
nodePath: path.join(dir, 'node'),
npmCli: path.join(npm, 'bin/npm-cli.js'),
licensePath: path.join(dir, 'LICENSE'),
nodeVersion,
};
};
try {
return run(root, {
arm64: sourceFor('arm64', 'v22.23.2'),
x64: sourceFor('x64', 'v22.23.2'),
});
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}
function optionsFor(root, sources, overrides = {}) {
const archOf = (file) => (file.includes('source-x64') ? 'x64' : 'arm64');
return {
destination: path.join(root, 'resources/node-runtime'),
hostPlatform: 'darwin',
hostArch: 'arm64',
hostNodePath: sources.arm64.nodePath,
hostNpmCli: sources.arm64.npmCli,
env: {
AGC_NODE_RUNTIME_DARWIN_X64_PATH: sources.x64.nodePath,
AGC_NODE_RUNTIME_DARWIN_X64_LICENSE_PATH: sources.x64.licensePath,
},
installedLicense() {
throw new Error('no installed fixture license');
},
execute(file, args) {
if (file === '/usr/bin/otool')
return `\t/usr/lib/libSystem.B.dylib\n\t/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation\n`;
if (args[0] === '-p') {
const arch = archOf(file);
return JSON.stringify({
platform: 'darwin',
arch,
version: sources[arch].nodeVersion,
});
}
return '11.0.0';
},
...overrides,
};
}
test('universal stages one runtime per architecture with architecture-correct manifests', () =>
universalFixture((root, sources) => {
assert.deepEqual(targetRuntimes('universal-apple-darwin'), [
'aarch64-apple-darwin',
'x86_64-apple-darwin',
]);
const manifests = stageNodeRuntimeForTarget(
'universal-apple-darwin',
optionsFor(root, sources),
);
assert.deepEqual(
manifests.map((manifest) => `${manifest.platform}-${manifest.arch}`),
['darwin-arm64', 'darwin-x64'],
);
for (const manifest of manifests) {
const directory = path.join(
root,
'resources/node-runtime',
`${manifest.platform}-${manifest.arch}`,
);
assert.equal(
JSON.parse(
fs.readFileSync(path.join(directory, 'manifest.json'), 'utf8'),
).arch,
manifest.arch,
);
assert.ok(fs.existsSync(path.join(directory, 'node')));
assert.ok(
fs.existsSync(path.join(directory, 'node_modules/npm/LICENSE')),
);
}
// 单架构目标仍写扁平目录(与既有发布一致),不产生分架构子目录。
const flat = stageNodeRuntimeForTarget(
'aarch64-apple-darwin',
optionsFor(root, sources, { destination: path.join(root, 'flat') }),
);
assert.equal(flat.length, 1);
assert.ok(fs.existsSync(path.join(root, 'flat/manifest.json')));
assert.ok(!fs.existsSync(path.join(root, 'flat/darwin-arm64')));
}));
test('universal fails closed when the non-host architecture runtime is absent', () =>
universalFixture((root, sources) => {
const options = optionsFor(root, sources, { env: {} });
assert.throws(
() => stageNodeRuntimeForTarget('universal-apple-darwin', options),
/AGC_NODE_RUNTIME_DARWIN_X64_PATH/u,
);
// 缺失时不得留下半成品目录。
assert.ok(!fs.existsSync(path.join(root, 'resources')));
}));
test('universal rejects mismatched Node versions between the two architectures', () =>
universalFixture((root, sources) => {
const options = optionsFor(root, sources);
options.execute = (file, args) => {
if (file === '/usr/bin/otool') return '\t/usr/lib/libSystem.B.dylib\n';
if (args[0] === '-p')
return JSON.stringify({
platform: 'darwin',
arch: file.includes('source-x64') ? 'x64' : 'arm64',
version: file.includes('source-x64') ? 'v22.23.2' : 'v24.0.0',
});
return '11.0.0';
};
assert.throws(
() => stageNodeRuntimeForTarget('universal-apple-darwin', options),
/版本不一致/u,
);
// 版本核对在写出任何架构之前完成,失败时不留下半套运行时。
assert.ok(!fs.existsSync(path.join(root, 'resources')));
}));
@@ -290,15 +290,37 @@ fn resolve_at(
let root = root
.canonicalize()
.map_err(|_| "project-root-unavailable")?;
if let Some(bundle) = bundle.filter(|bundle| bundle.exists()) {
validate_bundle(bundle)?;
return runtime_from_paths(
&root,
bundle.join(executable_name()),
bundle.join("node_modules/npm/bin/npm-cli.js"),
"bundled",
path,
);
// 候选顺序:既有扁平布局(单架构构建),其后是按**当前运行架构**命名的子目录
// universal 构建把两套运行时并列放进去)。两个候选都要过 manifest 的平台/架构
// 校验,因此顺序不会让另一架构的运行时被采用;存在但校验失败则失败关闭,
// 绝不因此退回系统 Node。
let mut bundle_error: Option<String> = None;
if let Some(base) = bundle {
let candidates = [
base.to_path_buf(),
base.join(format!("{}-{}", native_platform(), native_arch())),
];
for candidate in candidates.iter().filter(|candidate| candidate.exists()) {
match validate_bundle(candidate) {
Ok(()) => {
return runtime_from_paths(
&root,
candidate.join(executable_name()),
candidate.join("node_modules/npm/bin/npm-cli.js"),
"bundled",
path,
);
}
Err(error) => {
if bundle_error.is_none() {
bundle_error = Some(error);
}
}
}
}
}
if let Some(error) = bundle_error {
return Err(error);
}
if !development {
return Err("node-runtime-bundle-missing".into());
@@ -595,6 +617,52 @@ mod tests {
);
}
/// universal 构建把两套架构运行时并列放在 `<base>/<platform>-<arch>/`
/// 解析必须按当前运行架构选中自己那一份,且另一架构的存在与否不影响结果。
#[test]
fn universal_bundle_resolves_the_directory_matching_the_running_architecture() {
let project = tempfile::tempdir().unwrap();
let bundle = tempfile::tempdir().unwrap();
let current = bundle
.path()
.join(format!("{}-{}", native_platform(), native_arch()));
fs::create_dir_all(&current).unwrap();
bundle_fixture(&current);
let runtime = resolve_at(project.path(), Some(bundle.path()), false, OsStr::new(""))
.expect("universal bundle must resolve the running architecture");
assert_eq!(runtime.source, "bundled");
assert_eq!(
runtime.node.canonicalize().unwrap(),
current.join(executable_name()).canonicalize().unwrap()
);
// 另一架构的目录即使损坏也不能影响本架构选择。
let other = if native_arch() == "arm64" {
"x86_64"
} else {
"arm64"
};
let other_dir = bundle
.path()
.join(format!("{}-{}", native_platform(), other));
fs::create_dir_all(&other_dir).unwrap();
fs::write(other_dir.join("manifest.json"), b"not-json").unwrap();
assert_eq!(
resolve_at(project.path(), Some(bundle.path()), false, OsStr::new(""))
.unwrap()
.node
.canonicalize()
.unwrap(),
current.join(executable_name()).canonicalize().unwrap()
);
// 本架构目录缺失而另一架构完整时,绝不采用另一架构的运行时。
fs::remove_dir_all(&current).unwrap();
bundle_fixture(&other_dir);
assert!(resolve_at(project.path(), Some(bundle.path()), false, OsStr::new("")).is_err());
}
#[test]
fn development_runtime_rejects_relative_and_project_path_entries() {
let root = tempfile::tempdir().unwrap();