diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 09b5f06ae..c0d6943e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -150,6 +150,9 @@ jobs: - name: Install dependencies run: pnpm install + - name: Verify OpenDAL native package for Linux + run: pnpm run smoke:opendal:native -- --platform linux --arch ${{ matrix.arch }} + # - name: Install Node Runtime # run: pnpm run installRuntime:linux:${{ matrix.arch }} @@ -176,6 +179,9 @@ jobs: test -f "$EXTENSION_PATH" pnpm run smoke:duckdb:vss -- --platform linux --arch ${{ matrix.arch }} --extension-path "$EXTENSION_PATH" + - name: Verify packaged OpenDAL native package for Linux + run: pnpm run smoke:opendal:native -- --platform linux --arch ${{ matrix.arch }} --resources-path dist/linux-unpacked/resources + - name: Verify bundled plugins shell: bash run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 823e25d66..232d7b334 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -246,6 +246,9 @@ jobs: - name: Install dependencies run: pnpm install + - name: Verify OpenDAL native package for Linux + run: pnpm run smoke:opendal:native -- --platform linux --arch ${{ matrix.arch }} + - name: Install and verify DuckDB VSS for Linux run: | pnpm run installRuntime:duckdb:vss -- --platform linux --arch ${{ matrix.arch }} @@ -270,6 +273,9 @@ jobs: test -f "$EXTENSION_PATH" pnpm run smoke:duckdb:vss -- --platform linux --arch ${{ matrix.arch }} --extension-path "$EXTENSION_PATH" + - name: Verify packaged OpenDAL native package for Linux + run: pnpm run smoke:opendal:native -- --platform linux --arch ${{ matrix.arch }} --resources-path dist/linux-unpacked/resources + - name: Verify bundled plugins shell: bash run: | diff --git a/package.json b/package.json index b32a85f60..8a1e6ebe2 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "installRuntime:duckdb:vss:linux:x64": "node scripts/installVss.js --platform linux --arch x64", "installRuntime:duckdb:vss:linux:arm64": "node scripts/installVss.js --platform linux --arch arm64", "smoke:duckdb:vss": "node scripts/smoke-duckdb-vss.js", + "smoke:opendal:native": "node scripts/smoke-opendal-native.js", "i18n": "i18n-check -s zh-CN -f i18next --locales src/renderer/src/i18n", "i18n:en": "i18n-check -s en-US -f i18next --locales src/renderer/src/i18n", "i18n:types": "node scripts/generate-i18n-types.js", @@ -128,7 +129,7 @@ "nanoid": "^5.1.11", "node-pty": "1.2.0-beta.14", "ollama": "^0.6.3", - "opendal": "^0.49.4", + "opendal": "0.49.2", "pdf-parse-new": "^1.4.1", "qrcode": "^1.5.4", "run-applescript": "^7.1.0", @@ -220,14 +221,14 @@ "yaml": "^2.9.0" }, "optionalDependencies": { - "@opendal/lib-darwin-arm64": "0.49.4", - "@opendal/lib-darwin-x64": "0.49.4", - "@opendal/lib-linux-arm64-gnu": "0.49.4", - "@opendal/lib-linux-arm64-musl": "0.49.4", - "@opendal/lib-linux-x64-gnu": "0.49.4", - "@opendal/lib-linux-x64-musl": "0.49.4", - "@opendal/lib-win32-arm64-msvc": "0.49.4", - "@opendal/lib-win32-x64-msvc": "0.49.4" + "@opendal/lib-darwin-arm64": "0.49.2", + "@opendal/lib-darwin-x64": "0.49.2", + "@opendal/lib-linux-arm64-gnu": "0.49.2", + "@opendal/lib-linux-arm64-musl": "0.49.2", + "@opendal/lib-linux-x64-gnu": "0.49.2", + "@opendal/lib-linux-x64-musl": "0.49.2", + "@opendal/lib-win32-arm64-msvc": "0.49.2", + "@opendal/lib-win32-x64-msvc": "0.49.2" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/scripts/afterPack.js b/scripts/afterPack.js index f4a308369..b3054cd33 100644 --- a/scripts/afterPack.js +++ b/scripts/afterPack.js @@ -72,6 +72,31 @@ function getParcelWatcherBinaryPackages(platform, arch) { } } +function getOpendalNativePackages(platform, arch) { + const archName = getArchName(arch) + + if (platform === 'darwin' && archName === 'universal') { + return ['@opendal/lib-darwin-x64', '@opendal/lib-darwin-arm64'] + } + + switch (`${platform}:${archName}`) { + case 'darwin:x64': + return ['@opendal/lib-darwin-x64'] + case 'darwin:arm64': + return ['@opendal/lib-darwin-arm64'] + case 'win32:x64': + return ['@opendal/lib-win32-x64-msvc'] + case 'win32:arm64': + return ['@opendal/lib-win32-arm64-msvc'] + case 'linux:x64': + return ['@opendal/lib-linux-x64-gnu'] + case 'linux:arm64': + return ['@opendal/lib-linux-arm64-gnu'] + default: + return [] + } +} + async function pathExists(filePath) { try { await fs.access(filePath) @@ -176,6 +201,34 @@ async function copyParcelWatcherNativePackages(context) { } } +async function copyOpendalNativePackages(context) { + const { arch, electronPlatformName, packager } = context + const packageNames = getOpendalNativePackages(electronPlatformName, arch) + + if (packageNames.length === 0) { + return + } + + const nodeModulesDir = path.join(getResourcesDir(context), 'app.asar.unpacked', 'node_modules') + const opendalDir = path.join(nodeModulesDir, 'opendal') + + if (!(await pathExists(opendalDir))) { + throw new Error( + `Missing unpacked opendal at ${opendalDir}. Check electron-builder asarUnpack configuration.` + ) + } + + const projectDir = packager?.projectDir ?? process.cwd() + + for (const packageName of packageNames) { + const sourceDir = await resolveInstalledPackageDir(projectDir, packageName) + const destinationDir = path.join(nodeModulesDir, ...packageName.split('/')) + + await fs.mkdir(path.dirname(destinationDir), { recursive: true }) + await fs.cp(sourceDir, destinationDir, { recursive: true, force: true, dereference: true }) + } +} + function isLinux(targets) { const re = /AppImage|snap|deb|rpm|freebsd|pacman/i return !!targets.find((target) => re.test(target.name)) @@ -220,6 +273,7 @@ async function afterPack(context) { await copyFffNativePackages(context) await copyParcelWatcherNativePackages(context) + await copyOpendalNativePackages(context) await encodeMacVssExtension(context) if (isLinux(targets)) { diff --git a/scripts/install-sharp-for-platform.js b/scripts/install-sharp-for-platform.js index 8d73a46e1..f05ca4607 100644 --- a/scripts/install-sharp-for-platform.js +++ b/scripts/install-sharp-for-platform.js @@ -2,7 +2,6 @@ /** * Update pnpm-workspace.yaml supportedArchitectures for different platforms - * 根据不同平台动态修改 pnpm-workspace.yaml 的 supportedArchitectures 配置 */ import { readFileSync, writeFileSync, existsSync } from 'fs'; @@ -27,11 +26,13 @@ const platformConfigs = { }, 'linux-x64': { os: ['current', 'linux'], - cpu: ['current', 'wasm32'], // Include wasm32 for Sharp WebAssembly + cpu: ['current', 'x64', 'wasm32'], // Include wasm32 for Sharp WebAssembly + libc: ['glibc'], }, 'linux-arm64': { - os: ['current','linux'], - cpu: ['current', 'wasm32'], + os: ['current', 'linux'], + cpu: ['current', 'arm64', 'wasm32'], + libc: ['glibc'], }, 'darwin-x64': { os: ['current', 'darwin'], @@ -74,6 +75,9 @@ try { os: config.os, cpu: config.cpu }; + if (config.libc) { + workspaceConfig.supportedArchitectures.libc = config.libc; + } // Convert back to YAML with proper formatting const finalContent = YAML.stringify(workspaceConfig, { @@ -87,6 +91,9 @@ try { console.log(`📋 Configuration:`); console.log(` OS: ${config.os.join(', ')}`); console.log(` CPU: ${config.cpu.join(', ')}`); + if (config.libc) { + console.log(` Libc: ${config.libc.join(', ')}`); + } } catch (error) { console.error(`❌ Failed to update pnpm-workspace.yaml: ${error.message}`); process.exit(1); diff --git a/scripts/smoke-opendal-native.js b/scripts/smoke-opendal-native.js new file mode 100644 index 000000000..3fc7e882f --- /dev/null +++ b/scripts/smoke-opendal-native.js @@ -0,0 +1,187 @@ +#!/usr/bin/env node + +import fs from 'node:fs' +import path from 'node:path' +import { createRequire } from 'node:module' +import { pathToFileURL } from 'node:url' + +export function parseArgs(argv) { + const options = {} + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index] + if (arg === '--') continue + if (!arg.startsWith('--')) continue + const [rawKey, inlineValue] = arg.slice(2).split('=', 2) + let value = inlineValue + if (value === undefined) { + const next = argv[index + 1] + if (next === undefined || next === '--' || next.startsWith('--')) { + throw new Error(`Missing value for --${rawKey}`) + } + value = next + index += 1 + } + options[rawKey] = value + } + return options +} + +function normalizePlatform(value) { + switch (value) { + case 'darwin': + case 'mac': + case 'macos': + case 'osx': + return 'darwin' + case 'win32': + case 'windows': + case 'win': + return 'win32' + case 'linux': + return 'linux' + default: + throw new Error(`Unsupported OpenDAL platform: ${value}`) + } +} + +function normalizeArch(value) { + switch (value) { + case 'x64': + case 'amd64': + return 'x64' + case 'arm64': + case 'aarch64': + return 'arm64' + default: + throw new Error(`Unsupported OpenDAL architecture: ${value}`) + } +} + +function getOpendalNativePackage(platform, arch) { + switch (`${platform}:${arch}`) { + case 'darwin:x64': + return '@opendal/lib-darwin-x64' + case 'darwin:arm64': + return '@opendal/lib-darwin-arm64' + case 'win32:x64': + return '@opendal/lib-win32-x64-msvc' + case 'win32:arm64': + return '@opendal/lib-win32-arm64-msvc' + case 'linux:x64': + return '@opendal/lib-linux-x64-gnu' + case 'linux:arm64': + return '@opendal/lib-linux-arm64-gnu' + default: + throw new Error(`Unsupported OpenDAL target: ${platform}/${arch}`) + } +} + +function packagePathParts(packageName) { + return packageName.split('/') +} + +function resolvePackageDirFromNodeModules(nodeModulesDir, packageName) { + const directDir = path.join(nodeModulesDir, ...packagePathParts(packageName)) + if (fs.existsSync(path.join(directDir, 'package.json'))) { + return fs.realpathSync(directDir) + } + + const pnpmNodeModulesDir = path.join(nodeModulesDir, '.pnpm', 'node_modules', ...packagePathParts(packageName)) + if (fs.existsSync(path.join(pnpmNodeModulesDir, 'package.json'))) { + return fs.realpathSync(pnpmNodeModulesDir) + } + + const pnpmVirtualStoreDir = path.join(nodeModulesDir, '.pnpm') + if (fs.existsSync(pnpmVirtualStoreDir)) { + for (const entry of fs.readdirSync(pnpmVirtualStoreDir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue + const candidate = path.join( + pnpmVirtualStoreDir, + entry.name, + 'node_modules', + ...packagePathParts(packageName) + ) + if (fs.existsSync(path.join(candidate, 'package.json'))) { + return fs.realpathSync(candidate) + } + } + } + + throw new Error(`OpenDAL package ${packageName} not found under ${nodeModulesDir}`) +} + +function assertPackageMainExists(packageDir, label) { + const packageJsonPath = path.join(packageDir, 'package.json') + if (!fs.existsSync(packageJsonPath)) { + throw new Error(`${label} package.json not found at ${packageJsonPath}`) + } + + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) + if (!packageJson.main) { + throw new Error(`${label} package.json has no main field at ${packageJsonPath}`) + } + + const mainPath = path.join(packageDir, packageJson.main) + if (!fs.existsSync(mainPath)) { + throw new Error(`${label} native entry not found at ${mainPath}`) + } + return mainPath +} + +function maybeLoadOpendal(opendalDir, platform, arch, label) { + if (platform !== process.platform || arch !== process.arch) { + console.log( + `[OpenDAL Smoke] ${label}: target ${platform}/${arch} differs from host ${process.platform}/${process.arch}; verified file presence only.` + ) + return + } + + const opendalEntry = path.join(opendalDir, 'index.cjs') + const requireFromOpendal = createRequire(opendalEntry) + requireFromOpendal(opendalEntry) + console.log(`[OpenDAL Smoke] ${label}: loaded opendal from ${opendalEntry}`) +} + +function smokeNodeModules({ nodeModulesDir, platform, arch, label }) { + const nativePackageName = getOpendalNativePackage(platform, arch) + const opendalDir = resolvePackageDirFromNodeModules(nodeModulesDir, 'opendal') + const nativePackageDir = resolvePackageDirFromNodeModules(nodeModulesDir, nativePackageName) + const nativeEntry = assertPackageMainExists(nativePackageDir, `${label} ${nativePackageName}`) + + console.log(`[OpenDAL Smoke] ${label}: found ${nativePackageName} at ${nativePackageDir}`) + console.log(`[OpenDAL Smoke] ${label}: native entry ${nativeEntry}`) + maybeLoadOpendal(opendalDir, platform, arch, label) +} + +function main() { + const args = parseArgs(process.argv.slice(2)) + const platform = args.platform ? normalizePlatform(args.platform) : process.platform + const arch = args.arch ? normalizeArch(args.arch) : process.arch + const projectDir = path.resolve(args.projectDir ?? args['project-dir'] ?? process.cwd()) + const resourcesPath = args.resourcesPath ?? args['resources-path'] + + smokeNodeModules({ + nodeModulesDir: path.join(projectDir, 'node_modules'), + platform, + arch, + label: 'source' + }) + + if (resourcesPath) { + smokeNodeModules({ + nodeModulesDir: path.join(path.resolve(resourcesPath), 'app.asar.unpacked', 'node_modules'), + platform, + arch, + label: 'packaged' + }) + } +} + +if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) { + try { + main() + } catch (error) { + console.error('[OpenDAL Smoke] failed:', error) + process.exit(1) + } +} diff --git a/test/main/build/electronBuilderConfig.test.ts b/test/main/build/electronBuilderConfig.test.ts index a7a948181..2730055b0 100644 --- a/test/main/build/electronBuilderConfig.test.ts +++ b/test/main/build/electronBuilderConfig.test.ts @@ -7,22 +7,59 @@ interface ElectronBuilderConfig { asarUnpack?: string[] } +interface PackageJson { + dependencies?: Record + optionalDependencies?: Record +} + +const OPENDAL_VERSION = '0.49.2' +const OPENDAL_NATIVE_PACKAGES = [ + '@opendal/lib-darwin-arm64', + '@opendal/lib-darwin-x64', + '@opendal/lib-linux-arm64-gnu', + '@opendal/lib-linux-arm64-musl', + '@opendal/lib-linux-x64-gnu', + '@opendal/lib-linux-x64-musl', + '@opendal/lib-win32-arm64-msvc', + '@opendal/lib-win32-x64-msvc' +] as const + const readElectronBuilderConfig = async () => { const configPath = path.join(process.cwd(), 'electron-builder.yml') return parse(await readFile(configPath, 'utf8')) as ElectronBuilderConfig } +const readPackageJson = async () => { + const packageJsonPath = path.join(process.cwd(), 'package.json') + return JSON.parse(await readFile(packageJsonPath, 'utf8')) as PackageJson +} + describe('electron-builder config', () => { - it('unpacks FFF native dependencies for packaged app loading and signing', async () => { + it('unpacks native dependencies for packaged app loading and signing', async () => { const config = await readElectronBuilderConfig() expect(config.asarUnpack).toEqual( expect.arrayContaining([ '**/node_modules/@ff-labs/fff-node/**/*', '**/node_modules/@ff-labs/fff-bin-*/**/*', + '**/node_modules/opendal/**/*', + '**/node_modules/@opendal/**/*', '**/node_modules/ffi-rs/**/*', '**/node_modules/@yuuang/ffi-rs-*/**/*' ]) ) }) + + it('pins OpenDAL native packages to the Ubuntu 22.04 compatible ABI version', async () => { + const packageJson = await readPackageJson() + + expect(packageJson.dependencies?.opendal).toBe(OPENDAL_VERSION) + expect(Object.keys(packageJson.optionalDependencies ?? {}).sort()).toEqual( + expect.arrayContaining([...OPENDAL_NATIVE_PACKAGES].sort()) + ) + + for (const packageName of OPENDAL_NATIVE_PACKAGES) { + expect(packageJson.optionalDependencies?.[packageName]).toBe(OPENDAL_VERSION) + } + }) }) diff --git a/test/main/scripts/afterPack.test.ts b/test/main/scripts/afterPack.test.ts index dd1dbdad1..299cf4d7c 100644 --- a/test/main/scripts/afterPack.test.ts +++ b/test/main/scripts/afterPack.test.ts @@ -19,6 +19,81 @@ const loadAfterPack = async () => { }) => Promise } +const packageDir = (nodeModulesDir: string, packageName: string) => + path.join(nodeModulesDir, ...packageName.split('/')) + +const writePackage = async ( + nodeModulesDir: string, + packageName: string, + files: Record = {} +) => { + const dir = packageDir(nodeModulesDir, packageName) + await mkdir(dir, { recursive: true }) + await writeFile(path.join(dir, 'package.json'), `{"name":"${packageName}"}`) + for (const [relativePath, body] of Object.entries(files)) { + const filePath = path.join(dir, relativePath) + await mkdir(path.dirname(filePath), { recursive: true }) + await writeFile(filePath, body) + } +} + +const writeVirtualPackage = async ( + projectDir: string, + packageName: string, + files: Record = {} +) => { + await writePackage(path.join(projectDir, 'node_modules', '.pnpm', 'node_modules'), packageName, files) +} + +const writeUnpackedPackage = async ( + nodeModulesDir: string, + packageName: string, + files: Record = {} +) => { + await writePackage(nodeModulesDir, packageName, files) +} + +const seedDarwinNativePrerequisites = async ( + projectDir: string, + nodeModulesDir: string, + archName: 'arm64' | 'x64' +) => { + const fffPackageDir = `fff-bin-darwin-${archName}` + const parcelPackageDir = `watcher-darwin-${archName}` + const opendalPackageDir = `lib-darwin-${archName}` + + await writeVirtualPackage(projectDir, `@ff-labs/${fffPackageDir}`, { + 'libfff_c.dylib': 'native' + }) + await writeVirtualPackage(projectDir, `@parcel/${parcelPackageDir}`, { + 'watcher.node': 'parcel-native' + }) + await writeVirtualPackage(projectDir, `@opendal/${opendalPackageDir}`, { + [`opendal.darwin-${archName}.node`]: 'opendal-native' + }) + await writeUnpackedPackage(nodeModulesDir, '@ff-labs/fff-node') + await writeUnpackedPackage(nodeModulesDir, '@parcel/watcher') + await writeUnpackedPackage(nodeModulesDir, 'opendal', { + 'index.cjs': 'module.exports = {}' + }) + + return { fffPackageDir, parcelPackageDir, opendalPackageDir } +} + +const seedLinuxNativePrerequisites = async (projectDir: string, nodeModulesDir: string) => { + await writeVirtualPackage(projectDir, '@ff-labs/fff-bin-linux-x64-gnu', { + 'libfff_c.so': 'native' + }) + await writeVirtualPackage(projectDir, '@parcel/watcher-linux-x64-glibc', { + 'watcher.node': 'parcel-native' + }) + await writeUnpackedPackage(nodeModulesDir, '@ff-labs/fff-node') + await writeUnpackedPackage(nodeModulesDir, '@parcel/watcher') + await writeUnpackedPackage(nodeModulesDir, 'opendal', { + 'index.cjs': 'module.exports = {}' + }) +} + describe('afterPack', () => { let tmpDir: string @@ -99,77 +174,98 @@ describe('afterPack', () => { }) it.each([ - ['arm64', 3, 'fff-bin-darwin-arm64', 'watcher-darwin-arm64'], - ['x64', 1, 'fff-bin-darwin-x64', 'watcher-darwin-x64'] - ])( - 'copies native packages into unpacked mac %s app node_modules', - async (_, arch, fffPackageDir, parcelPackageDir) => { - const afterPack = await loadAfterPack() - const projectDir = path.join(tmpDir, 'project') - const fffSourceDir = path.join( - projectDir, - 'node_modules', - '.pnpm', - 'node_modules', - '@ff-labs', - fffPackageDir - ) - const parcelSourceDir = path.join( + ['arm64', 3], + ['x64', 1] + ] as const)('copies native packages into unpacked mac %s app node_modules', async (archName, arch) => { + const afterPack = await loadAfterPack() + const projectDir = path.join(tmpDir, 'project') + const nodeModulesDir = path.join( + tmpDir, + 'DeepChat.app', + 'Contents', + 'Resources', + 'app.asar.unpacked', + 'node_modules' + ) + const { fffPackageDir, parcelPackageDir, opendalPackageDir } = + await seedDarwinNativePrerequisites(projectDir, nodeModulesDir, archName) + + await writeFile(path.join(tmpDir, 'DeepChat'), 'launcher') + + await afterPack({ + targets: [], + appOutDir: tmpDir, + electronPlatformName: 'darwin', + arch, + packager: { projectDir, - 'node_modules', - '.pnpm', - 'node_modules', - '@parcel', - parcelPackageDir - ) - const nodeModulesDir = path.join( - tmpDir, - 'DeepChat.app', - 'Contents', - 'Resources', - 'app.asar.unpacked', - 'node_modules' - ) + appInfo: { + productFilename: 'DeepChat' + } + } + }) - await writeFile(path.join(tmpDir, 'DeepChat'), 'launcher') - await mkdir(fffSourceDir, { recursive: true }) - await mkdir(parcelSourceDir, { recursive: true }) - await mkdir(path.join(nodeModulesDir, '@ff-labs', 'fff-node'), { recursive: true }) - await mkdir(path.join(nodeModulesDir, '@parcel', 'watcher'), { recursive: true }) - await writeFile( - path.join(fffSourceDir, 'package.json'), - `{"name":"@ff-labs/${fffPackageDir}"}` + await expect( + readFile(path.join(nodeModulesDir, '@ff-labs', fffPackageDir, 'libfff_c.dylib'), 'utf8') + ).resolves.toBe('native') + await expect( + readFile(path.join(nodeModulesDir, '@parcel', parcelPackageDir, 'watcher.node'), 'utf8') + ).resolves.toBe('parcel-native') + await expect( + readFile( + path.join(nodeModulesDir, '@opendal', opendalPackageDir, `opendal.darwin-${archName}.node`), + 'utf8' ) - await writeFile( - path.join(parcelSourceDir, 'package.json'), - `{"name":"@parcel/${parcelPackageDir}"}` + ).resolves.toBe('opendal-native') + }) + + it('copies OpenDAL Linux x64 native package into unpacked app node_modules', async () => { + const afterPack = await loadAfterPack() + const projectDir = path.join(tmpDir, 'project') + const nodeModulesDir = path.join(tmpDir, 'resources', 'app.asar.unpacked', 'node_modules') + + await seedLinuxNativePrerequisites(projectDir, nodeModulesDir) + await writeVirtualPackage(projectDir, '@opendal/lib-linux-x64-gnu', { + 'opendal.linux-x64-gnu.node': 'opendal-native' + }) + + await afterPack({ + targets: [], + appOutDir: tmpDir, + electronPlatformName: 'linux', + arch: 'x64', + packager: { + projectDir + } + }) + + await expect( + readFile( + path.join(nodeModulesDir, '@opendal', 'lib-linux-x64-gnu', 'opendal.linux-x64-gnu.node'), + 'utf8' ) - await writeFile(path.join(fffSourceDir, 'libfff_c.dylib'), 'native') - await writeFile(path.join(parcelSourceDir, 'watcher.node'), 'parcel-native') - await writeFile(path.join(nodeModulesDir, '@ff-labs', 'fff-node', 'package.json'), '{}') - await writeFile(path.join(nodeModulesDir, '@parcel', 'watcher', 'package.json'), '{}') + ).resolves.toBe('opendal-native') + }) + + it('fails fast when the target OpenDAL native package is missing', async () => { + const afterPack = await loadAfterPack() + const projectDir = path.join(tmpDir, 'project') + const nodeModulesDir = path.join(tmpDir, 'resources', 'app.asar.unpacked', 'node_modules') + + await seedLinuxNativePrerequisites(projectDir, nodeModulesDir) - await afterPack({ + await expect( + afterPack({ targets: [], appOutDir: tmpDir, - electronPlatformName: 'darwin', - arch, + electronPlatformName: 'linux', + arch: 'x64', packager: { - projectDir, - appInfo: { - productFilename: 'DeepChat' - } + projectDir } }) - - await expect( - readFile(path.join(nodeModulesDir, '@ff-labs', fffPackageDir, 'libfff_c.dylib'), 'utf8') - ).resolves.toBe('native') - await expect( - readFile(path.join(nodeModulesDir, '@parcel', parcelPackageDir, 'watcher.node'), 'utf8') - ).resolves.toBe('parcel-native') - } - ) + ).rejects.toThrow('Unable to find installed native package: @opendal/lib-linux-x64-gnu') + }) it('fails fast when FFF node output is missing for supported packages', async () => { const afterPack = await loadAfterPack() diff --git a/test/main/scripts/installSharpForPlatform.test.ts b/test/main/scripts/installSharpForPlatform.test.ts new file mode 100644 index 000000000..474c6a6bf --- /dev/null +++ b/test/main/scripts/installSharpForPlatform.test.ts @@ -0,0 +1,44 @@ +import { execFileSync } from 'child_process' +import { mkdtemp, readFile, rm, writeFile } from 'fs/promises' +import os from 'os' +import path from 'path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { parse } from 'yaml' + +describe('install-sharp-for-platform', () => { + let tmpDir: string + + beforeEach(async () => { + tmpDir = await mkdtemp(path.join(os.tmpdir(), 'deepchat-install-sharp-')) + }) + + afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }) + }) + + it.each([ + ['x64', ['current', 'x64', 'wasm32']], + ['arm64', ['current', 'arm64', 'wasm32']] + ])('includes the target Linux %s CPU for optional native packages', async (arch, expectedCpu) => { + const workspacePath = path.join(tmpDir, 'pnpm-workspace.yaml') + await writeFile(workspacePath, "publicHoistPattern:\n - '@img/sharp-*'\n") + + execFileSync(process.execPath, [path.join(process.cwd(), 'scripts/install-sharp-for-platform.js')], { + cwd: tmpDir, + env: { + ...process.env, + TARGET_OS: 'linux', + TARGET_ARCH: arch + }, + stdio: 'pipe' + }) + + const workspaceConfig = parse(await readFile(workspacePath, 'utf8')) + + expect(workspaceConfig.supportedArchitectures).toEqual({ + os: ['current', 'linux'], + cpu: expectedCpu, + libc: ['glibc'] + }) + }) +})