`prepareDevelopmentProject()` resolved the build target from the host platform and architecture, so a Linux host threw `unsupported target linux-x64` while writing the runtime descriptor. Pass the target in instead: the launcher supplies the target it prepared, and isolated tests supply a fixed supported target. Extract `desktopTargetPlatform()` into desktop-build-paths.mjs so the target-to-payload mapping has one home, and use it for the qualification runner, which previously derived both its target directory and its descriptor from the host architecture. Correct the README limitation to name the module that now performs the architecture check.
170 lines
7.0 KiB
TypeScript
170 lines
7.0 KiB
TypeScript
/** Prepare the disposable npm-project view used by an unpackaged Electron shell. */
|
|
|
|
import {
|
|
existsSync,
|
|
lstatSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
realpathSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
unlinkSync,
|
|
writeFileSync,
|
|
} from 'node:fs'
|
|
import { dirname, join } from 'node:path'
|
|
import { createDevelopmentProjectMetadata } from '../src/project-manager.ts'
|
|
import type { DesktopRelease } from '../src/release.ts'
|
|
import { DESKTOP_RUNTIME_FILE, type DesktopRuntimeDescriptor } from '../src/runtime-tree.ts'
|
|
import type { DesktopAutoUpdateTarget } from './desktop-auto-update-environment.mjs'
|
|
import { desktopTargetPlatform } from './desktop-build-paths.mjs'
|
|
|
|
interface PackageManifest {
|
|
readonly name?: string
|
|
readonly version?: string
|
|
readonly dependencies?: Readonly<Record<string, string>>
|
|
}
|
|
|
|
/** Inputs whose locations differ between the launcher and isolated tests. */
|
|
export interface DevelopmentProjectOptions {
|
|
/** Directory replaced with the generated development project. */
|
|
readonly projectDir: string
|
|
/** Current workspace's `apps/cli` package directory. */
|
|
readonly cliDir: string
|
|
/** Current workspace's private Desktop Host application directory. */
|
|
readonly hostDir: string
|
|
/** pnpm's workspace-wide virtual-hoist directory. */
|
|
readonly dependencyDir: string
|
|
/** Release identity written into the disposable project metadata. */
|
|
readonly release: DesktopRelease
|
|
/** Build target whose prepared payload the disposable project runs against. */
|
|
readonly target: DesktopAutoUpdateTarget
|
|
}
|
|
|
|
function readManifest(path: string): PackageManifest {
|
|
return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
|
|
}
|
|
|
|
function removeOwnedPath(path: string): void {
|
|
let stat: ReturnType<typeof lstatSync>
|
|
try {
|
|
stat = lstatSync(path)
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
|
throw error
|
|
}
|
|
if (stat.isSymbolicLink()) {
|
|
unlinkSync(path)
|
|
return
|
|
}
|
|
if (stat.isDirectory()) {
|
|
rmSync(path, { recursive: true })
|
|
return
|
|
}
|
|
unlinkSync(path)
|
|
}
|
|
|
|
function linkDirectory(source: string, destination: string): void {
|
|
mkdirSync(dirname(destination), { recursive: true })
|
|
symlinkSync(realpathSync(source), destination, process.platform === 'win32' ? 'junction' : 'dir')
|
|
}
|
|
|
|
function mirrorDependencyLinks(sourceRoot: string, destinationRoot: string): string[] {
|
|
const names: string[] = []
|
|
for (const entry of readdirSync(sourceRoot, { withFileTypes: true })) {
|
|
if (entry.name === '.bin') continue
|
|
const source = join(sourceRoot, entry.name)
|
|
if (entry.name.startsWith('@') && (entry.isDirectory() || entry.isSymbolicLink())) {
|
|
mkdirSync(join(destinationRoot, entry.name), { recursive: true })
|
|
for (const scoped of readdirSync(source, { withFileTypes: true })) {
|
|
if (!scoped.isDirectory() && !scoped.isSymbolicLink()) continue
|
|
linkDirectory(join(source, scoped.name), join(destinationRoot, entry.name, scoped.name))
|
|
names.push(`${entry.name}/${scoped.name}`)
|
|
}
|
|
continue
|
|
}
|
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
linkDirectory(source, join(destinationRoot, entry.name))
|
|
names.push(entry.name)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
/** Configured workspace plugins must resolve from the profile even when pnpm does not hoist them. */
|
|
function mirrorWorkspaceDependencies(roots: readonly string[], destinationRoot: string): string[] {
|
|
const names: string[] = []
|
|
const visited = new Set<string>()
|
|
const visit = (directory: string): void => {
|
|
const source = realpathSync(directory)
|
|
if (visited.has(source)) return
|
|
visited.add(source)
|
|
const manifest = readManifest(join(source, 'package.json'))
|
|
for (const [name, specifier] of Object.entries(manifest.dependencies ?? {})) {
|
|
if (!specifier.startsWith('workspace:')) continue
|
|
const dependency = join(source, 'node_modules', ...name.split('/'))
|
|
if (!existsSync(dependency)) {
|
|
throw new Error(`desktop development: ${name} is missing from ${source}; run pnpm install`)
|
|
}
|
|
const destination = join(destinationRoot, ...name.split('/'))
|
|
removeOwnedPath(destination)
|
|
linkDirectory(dependency, destination)
|
|
names.push(name)
|
|
visit(dependency)
|
|
}
|
|
}
|
|
for (const directory of roots) visit(directory)
|
|
return names
|
|
}
|
|
|
|
/**
|
|
* Replace one disposable project with links to the current built workspace.
|
|
* @param options - Project destination, CLI package, release identity, and build target.
|
|
* @returns the absolute project directory supplied by the caller.
|
|
*/
|
|
export function prepareDevelopmentProject(options: DevelopmentProjectOptions): string {
|
|
const cliManifest = readManifest(join(options.cliDir, 'package.json'))
|
|
if (cliManifest.name !== '@deepseek-ai/dsh' || cliManifest.version !== options.release.version) {
|
|
throw new Error(
|
|
`desktop development: apps/cli must be @deepseek-ai/dsh@${options.release.version}, found `
|
|
+ `${String(cliManifest.name)}@${String(cliManifest.version)}`,
|
|
)
|
|
}
|
|
if (!existsSync(options.dependencyDir)) {
|
|
throw new Error('desktop development: workspace dependency links are missing; run pnpm install')
|
|
}
|
|
const hostManifest = readManifest(join(options.hostDir, 'package.json'))
|
|
if (hostManifest.name !== '@deepseek-ai/dsh-desktop-host' || hostManifest.version !== options.release.version) {
|
|
throw new Error(
|
|
`desktop development: apps/desktop-host must be @deepseek-ai/dsh-desktop-host@${options.release.version}, found `
|
|
+ `${String(hostManifest.name)}@${String(hostManifest.version)}`,
|
|
)
|
|
}
|
|
if (!existsSync(join(options.hostDir, 'lib', 'index.js'))) {
|
|
throw new Error('desktop development: apps/desktop-host/lib/index.js is missing; run pnpm run build')
|
|
}
|
|
|
|
removeOwnedPath(options.projectDir)
|
|
createDevelopmentProjectMetadata(options.projectDir, options.release)
|
|
const destinationModules = join(options.projectDir, 'node_modules')
|
|
mkdirSync(destinationModules, { recursive: true })
|
|
const names = [
|
|
...mirrorDependencyLinks(options.dependencyDir, destinationModules),
|
|
...mirrorWorkspaceDependencies([options.cliDir, options.hostDir], destinationModules),
|
|
]
|
|
const dshLink = join(destinationModules, '@deepseek-ai', 'dsh')
|
|
removeOwnedPath(dshLink)
|
|
linkDirectory(options.cliDir, dshLink)
|
|
const hostLink = join(destinationModules, '@deepseek-ai', 'dsh-desktop-host')
|
|
removeOwnedPath(hostLink)
|
|
linkDirectory(options.hostDir, hostLink)
|
|
const sharedPackages = [...new Set([...names, '@deepseek-ai/dsh', '@deepseek-ai/dsh-desktop-host'])].flatMap((name) => {
|
|
const manifest = readManifest(join(destinationModules, name, 'package.json'))
|
|
return typeof manifest.version === 'string' ? [{ name, version: manifest.version, path: `node_modules/${name}` }] : []
|
|
})
|
|
const runtime: DesktopRuntimeDescriptor = { schemaVersion: 1, release: options.release,
|
|
...desktopTargetPlatform(options.target), sharedPackages, files: [] }
|
|
writeFileSync(join(options.projectDir, DESKTOP_RUNTIME_FILE), `${JSON.stringify(runtime, undefined, 2)}\n`)
|
|
return options.projectDir
|
|
}
|