fix(app-boot): preserve profile modules inside pkg executables

Teach the profile installation fallback to use normal symlinks under Node and real ESM proxy packages under pkg. Each proxy records the source package version, mirrors its explicit runtime subpath exports, and re-exports the virtual /snapshot URLs, so built-in Loader rows and external plugin peers resolve one shared Cordis/module instance from an on-disk profile.

Keep proxy healing idempotent, reject foreign real directories, cover root and subpath imports in packaged mode, and expand AggregateError startup diagnostics so concurrent Loader failures retain their individual import causes. This is the reusable packaged-profile mechanism; Python-specific artifact wiring remains in the next commit.
This commit is contained in:
Tianyi Cui
2026-08-24 17:28:26 +08:00
parent 1d4dcf3b57
commit 809a4c5bad
4 changed files with 260 additions and 13 deletions
+4 -2
View File
@@ -1,5 +1,5 @@
/** /**
* Shared boot glue for `dsh` profiles and the temporarily packaged Python SDK runtime: load the gitignored * Shared boot glue for `dsh` profiles, including the CLI packaged by the Python runtime wheel: load the gitignored
* `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the * `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the
* optional user patch layers from the Harness home (`~/.dsh`), expose its path resolver to * optional user patch layers from the Harness home (`~/.dsh`), expose its path resolver to
* config expressions, and drive the Cordis Loader against a leaf `cordis.yml` until the tree settles. * config expressions, and drive the Cordis Loader against a leaf `cordis.yml` until the tree settles.
@@ -811,7 +811,9 @@ export async function boot(
// original activation error instead of only the wrap chain. // original activation error instead of only the wrap chain.
let deepest: unknown = cause let deepest: unknown = cause
while (deepest instanceof Error && deepest.cause !== undefined) deepest = deepest.cause while (deepest instanceof Error && deepest.cause !== undefined) deepest = deepest.cause
const stack = deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : '' const stack = deepest instanceof AggregateError
? `\n${deepest.stack ?? deepest.message}\n${deepest.errors.map(formatActivationError).join('\n')}`
: deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : ''
throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause }) throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause })
} }
} }
+136 -10
View File
@@ -24,9 +24,10 @@
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { import {
existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync,
} from 'node:fs' } from 'node:fs'
import { basename, dirname, join } from 'node:path' import { basename, dirname, join } from 'node:path'
import { pathToFileURL } from 'node:url'
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include' import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths' import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
@@ -238,22 +239,140 @@ function ensureSymlink(link: string, target: string): void {
} }
} }
interface ModuleProxyManifest {
name: string
version: string
private: true
type: 'module'
exports: Record<string, string>
dsh: { moduleFallback: { targets: Record<string, string> } }
}
interface ModuleProxyRecord {
version?: unknown
dsh?: { moduleFallback?: { targets?: unknown } }
}
/** Return whether the process reads application modules from pkg's virtual filesystem. */
function isPackagedExecutable(): boolean {
return (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined
}
/** Resolve one package specifier from the dsh installation. */
function packageEntryFromAnchor(anchor: string, specifier: string): string | undefined {
try {
return createRequire(anchor).resolve(specifier)
} catch {
return undefined
}
}
/** Resolve every explicit runtime export that an out-of-tree plugin can import. */
function packageProxySource(
installAnchor: string,
packageName: string,
packageDir: string,
): { version: string; targets: Record<string, string> } {
const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as {
exports?: unknown
version?: unknown
}
if (typeof manifest.version !== 'string' || manifest.version.length === 0) {
throw new Error(`dsh: installed package ${packageName} must declare a non-empty version`)
}
const declared = manifest.exports
const subpaths = declared !== null && typeof declared === 'object' && !Array.isArray(declared)
&& Object.keys(declared).some(key => key.startsWith('.'))
? Object.keys(declared).filter(key => key === '.' || (key.startsWith('./') && !key.includes('*') && key !== './package.json'))
: ['.']
const targets: Record<string, string> = {}
for (const subpath of subpaths) {
const specifier = subpath === '.' ? packageName : packageName + subpath.slice(1)
const entry = packageEntryFromAnchor(installAnchor, specifier)
if (entry !== undefined) targets[subpath] = pathToFileURL(entry).href
}
return { version: manifest.version, targets }
}
/**
* Materialize a real package proxy whose exports retain pkg's virtual module
* URL. Files outside the executable cannot traverse a symlink into
* `/snapshot`, while an ESM re-export can import that URL and preserves the
* executable's single module instance for out-of-tree plugin peers.
*/
function ensureModuleProxy(
link: string,
packageName: string,
version: string,
targets: Record<string, string>,
): void {
const proxyExports = Object.fromEntries(
Object.keys(targets).map((subpath, index) => [subpath, `./entry-${index}.js`]),
)
const manifest: ModuleProxyManifest = {
name: packageName,
version,
private: true,
type: 'module',
exports: proxyExports,
dsh: { moduleFallback: { targets } },
}
let stat
try {
stat = lstatSync(link)
} catch {
stat = undefined
}
if (stat?.isSymbolicLink()) {
unlinkSync(link)
stat = undefined
}
if (stat !== undefined) {
const marker = join(link, 'package.json')
let existing: ModuleProxyRecord | undefined
try {
existing = JSON.parse(readFileSync(marker, 'utf8')) as ModuleProxyRecord
} catch {
existing = undefined
}
if (existing?.dsh?.moduleFallback?.targets === undefined) {
throw new Error(`dsh: ${link} exists and is not a dsh-managed module proxy; remove it so dsh can manage the installation fallback`)
}
if (existing.version === version
&& JSON.stringify(existing.dsh.moduleFallback.targets) === JSON.stringify(targets)) return
rmSync(link, { recursive: true })
}
mkdirSync(link, { recursive: true })
writeFileSync(join(link, 'package.json'), JSON.stringify(manifest, undefined, 2) + '\n')
for (const [index, target] of Object.values(targets).entries()) {
const specifier = JSON.stringify(target)
writeFileSync(
join(link, `entry-${index}.js`),
`export * from ${specifier}\nimport * as target from ${specifier}\nexport default target.default\n`,
)
}
}
/** /**
* Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one * Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one
* symlink per package in the dsh app's resolvable dependency CLOSURE (BFS * entry per package in the dsh app's resolvable dependency CLOSURE (BFS
* over `dependencies` from the app manifest), each resolved from its own * over `dependencies` from the app manifest), each resolved from its own
* real location. Node's parent-directory walk from any profile finds this * installation location. Plain Node uses symlinks. A pkg executable writes
* small ESM proxy packages instead because the host filesystem cannot follow
* a symlink into pkg's virtual `/snapshot` tree; the proxy re-exports the
* virtual URL, preserving the executable's single module instance. Node's
* parent-directory walk from any profile finds this
* directory after the profile's own `node_modules`, so every in-box plugin * directory after the profile's own `node_modules`, so every in-box plugin
* resolves without pnpm ever managing it — the exact "bundles come from the * resolves without pnpm ever managing it — the exact "bundles come from the
* installation" contract. The closure (not just direct dependencies) is * installation" contract. The closure (not just direct dependencies) is
* required for out-of-tree plugins: their peer dependencies name Service * required for out-of-tree plugins: their peer dependencies name Service
* Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app * Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app
* reaches only through its Service Provider packages. Symlinked packages * reaches only through its Service Provider packages. Both a symlink target
* resolve their own dependencies from their real directories (Node's default * and a proxy's virtual target resolve transitive imports from the original
* symlink-following), so each package needs only its one flat link. * package directory, so each package needs one flat fallback entry.
* Idempotent: correct links are kept and moved installations are * Idempotent: correct entries are kept and changed installation targets are
* re-pointed; a stale link to a vanished package stays until its name is * rewritten; under plain Node, a stale dangling link stays until its name is
* reused (dangling links are invisible to resolution). * reused because resolution cannot discover it.
* @param installAnchor - absolute path of the dsh app's package.json. * @param installAnchor - absolute path of the dsh app's package.json.
* @param home - the Harness home; defaults to {@link resolveDshHome}. * @param home - the Harness home; defaults to {@link resolveDshHome}.
*/ */
@@ -287,7 +406,14 @@ export function healProfilesModuleFallback(installAnchor: string, home: string =
for (const [packageName, target] of links) { for (const [packageName, target] of links) {
const link = join(modulesDir, packageName) const link = join(modulesDir, packageName)
mkdirSync(dirname(link), { recursive: true }) mkdirSync(dirname(link), { recursive: true })
ensureSymlink(link, target) if (isPackagedExecutable()) {
const source = packageProxySource(installAnchor, packageName, target)
if (Object.keys(source.targets).length > 0) {
ensureModuleProxy(link, packageName, source.version, source.targets)
}
} else {
ensureSymlink(link, target)
}
} }
} }
@@ -771,6 +771,28 @@ describe('boot', () => {
) )
}) })
it('expands a stackless aggregate at the deepest activation cause', async () => {
const dir = tmp()
const aggregate = new AggregateError([
new Error('first aggregate member'),
'second aggregate member',
], 'aggregate activation failure')
delete (aggregate as { stack?: string }).stack
try {
await boot(NAME, join(dir, 'cordis.yml'), undefined, () => {
throw new Error('wrapped aggregate failure', { cause: aggregate })
})
expect.fail('boot should reject the aggregate activation failure')
} catch (error) {
expect(error).toBeInstanceOf(Error)
const message = (error as Error).message
expect(message).toContain(`${NAME}: host preparation failed: wrapped aggregate failure`)
expect(message).toContain('aggregate activation failure')
expect(message).toContain('first aggregate member')
expect(message).toContain('second aggregate member')
}
})
it('reports a pending real Loader fiber and the service unresolved in its own context', async () => { it('reports a pending real Loader fiber and the service unresolved in its own context', async () => {
const dir = tmp() const dir = tmp()
writeFileSync(join(dir, 'waiting.mjs'), 'export const inject = ["neverProvided"]\nexport function apply() {}\n') writeFileSync(join(dir, 'waiting.mjs'), 'export const inject = ["neverProvided"]\nexport function apply() {}\n')
+98 -1
View File
@@ -36,12 +36,18 @@ function stageInstallation(bundles: Record<string, { patch?: string; deps?: Reco
writeFileSync(join(dir, 'package.json'), JSON.stringify({ writeFileSync(join(dir, 'package.json'), JSON.stringify({
name, name,
version: '0.0.0', version: '0.0.0',
type: 'module',
main: './index.js',
dependencies: spec.deps ?? {}, dependencies: spec.deps ?? {},
...spec.patch === undefined ? {} : { dsh: { bundle: { patch: './cordis.patch.yml' } } }, ...spec.patch === undefined ? {} : { dsh: { bundle: { patch: './cordis.patch.yml' } } },
})) }))
writeFileSync(join(dir, 'index.js'), `export const packageName = ${JSON.stringify(name)}\n`)
if (spec.patch !== undefined) writeFileSync(join(dir, 'cordis.patch.yml'), spec.patch) if (spec.patch !== undefined) writeFileSync(join(dir, 'cordis.patch.yml'), spec.patch)
} }
writeFileSync(join(appDir, 'package.json'), JSON.stringify({ name: 'dsh-app', dependencies: appDeps })) writeFileSync(join(appDir, 'package.json'), JSON.stringify({
name: 'dsh-app', version: '0.0.0', type: 'module', main: './index.js', dependencies: appDeps,
}))
writeFileSync(join(appDir, 'index.js'), 'export const packageName = "dsh-app"\n')
return join(appDir, 'package.json') return join(appDir, 'package.json')
} }
@@ -322,4 +328,95 @@ describe('healProfilesModuleFallback', () => {
const fallback = join(home, 'profiles', 'node_modules') const fallback = join(home, 'profiles', 'node_modules')
expect(lstatSync(join(fallback, 'dsh-app')).isSymbolicLink()).toBe(true) expect(lstatSync(join(fallback, 'dsh-app')).isSymbolicLink()).toBe(true)
}) })
it('writes real ESM proxies for a packaged executable', async () => {
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
const bundleManifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
bundleManifest.exports = { '.': './index.js', './feature': './feature.js' }
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(bundleManifest))
writeFileSync(join(bundleDir, 'feature.js'), 'export const feature = "proxied"\n')
const home = tmp()
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
try {
healProfilesModuleFallback(anchor, home)
const fallback = join(home, 'profiles', 'node_modules')
const proxy = join(fallback, 'bundle-a')
expect(lstatSync(proxy).isDirectory()).toBe(true)
const proxyManifest = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as {
version: unknown
exports: unknown
dsh: { moduleFallback: { targets: Record<string, unknown> } }
}
expect(proxyManifest).toMatchObject({
version: '0.0.0',
exports: { '.': './entry-0.js', './feature': './entry-1.js' },
})
expect(proxyManifest.dsh.moduleFallback.targets['.']).toEqual(expect.stringContaining('/bundle-a/index.js'))
await expect(import(join(proxy, 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' })
await expect(import(join(proxy, 'entry-1.js'))).resolves.toMatchObject({ feature: 'proxied' })
healProfilesModuleFallback(anchor, home)
} finally {
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
}
})
it('requires a package version before writing a packaged proxy', () => {
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
manifest.version = ''
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
try {
expect(() => { healProfilesModuleFallback(anchor, tmp()) }).toThrow(
'installed package bundle-a must declare a non-empty version',
)
} finally {
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
}
})
it('replaces plain-node links and stale managed proxies in packaged mode', () => {
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
const home = tmp()
healProfilesModuleFallback(anchor, home)
const proxy = join(home, 'profiles', 'node_modules', 'bundle-a')
expect(lstatSync(proxy).isSymbolicLink()).toBe(true)
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
try {
healProfilesModuleFallback(anchor, home)
expect(lstatSync(proxy).isDirectory()).toBe(true)
const stale = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as {
version: string
}
stale.version = 'stale'
writeFileSync(join(proxy, 'package.json'), JSON.stringify(stale))
healProfilesModuleFallback(anchor, home)
expect(JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8'))).toMatchObject({
version: '0.0.0',
})
} finally {
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
}
})
it('rejects foreign packaged fallback directories with valid or invalid metadata', () => {
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
try {
for (const metadata of ['{}', '{']) {
const home = tmp()
const proxy = join(home, 'profiles', 'node_modules', 'bundle-a')
mkdirSync(proxy, { recursive: true })
writeFileSync(join(proxy, 'package.json'), metadata)
expect(() => { healProfilesModuleFallback(anchor, home) }).toThrow(
'exists and is not a dsh-managed module proxy',
)
}
} finally {
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
}
})
}) })