import * as path from 'node:path' import { Schema } from '@effect/schema' import { $ } from 'bun' import { Console, Context, Data, Effect, Layer } from 'effect' // ============================================================================ // Domain Models & Schema // ============================================================================ /** * Bun 构建目标后缀 */ const BunTargetSuffixSchema = Schema.Literal( 'windows-x64', 'darwin-arm64', 'darwin-x64', 'linux-x64', 'linux-arm64', ) type BunTargetSuffix = Schema.Schema.Type /** * Tauri sidecar 目标三元组 */ const TauriTargetSchema = Schema.Literal( 'x86_64-pc-windows-msvc', 'aarch64-apple-darwin', 'x86_64-apple-darwin', 'x86_64-unknown-linux-gnu', 'aarch64-unknown-linux-gnu', ) type TauriTarget = Schema.Schema.Type /** * 目标映射配置 */ const TargetMappingSchema = Schema.Struct({ bunSuffix: BunTargetSuffixSchema, tauriTarget: TauriTargetSchema, }) type TargetMapping = Schema.Schema.Type /** * 复制配置 */ const CopyConfigSchema = Schema.Struct({ sourceDir: Schema.String.pipe(Schema.nonEmptyString()), targetDir: Schema.String.pipe(Schema.nonEmptyString()), baseName: Schema.String.pipe(Schema.nonEmptyString()), mappings: Schema.Array(TargetMappingSchema).pipe(Schema.minItems(1)), }) type CopyConfig = Schema.Schema.Type /** * 复制结果 */ const CopyResultSchema = Schema.Struct({ bunSuffix: BunTargetSuffixSchema, tauriTarget: TauriTargetSchema, sourceFile: Schema.String, targetFile: Schema.String, success: Schema.Boolean, }) type CopyResult = Schema.Schema.Type // ============================================================================ // Error Models // ============================================================================ class ConfigError extends Data.TaggedError('ConfigError')<{ readonly message: string readonly cause: unknown }> {} class FileSystemError extends Data.TaggedError('FileSystemError')<{ readonly operation: string readonly path: string readonly cause: unknown }> {} class CopyError extends Data.TaggedError('CopyError')<{ readonly source: string readonly target: string readonly cause: unknown }> {} // ============================================================================ // Services // ============================================================================ /** * 配置服务 */ class CopyConfigService extends Context.Tag('CopyConfigService')< CopyConfigService, CopyConfig >() { /** * 从原始数据创建并验证配置 */ static fromRaw = (raw: unknown) => Effect.gen(function* () { const decoded = yield* Schema.decodeUnknown(CopyConfigSchema)(raw) return decoded }).pipe( Effect.catchAll((error) => Effect.fail( new ConfigError({ message: '配置验证失败', cause: error, }), ), ), ) /** * 默认配置 Layer */ static readonly Live = Layer.effect( CopyConfigService, CopyConfigService.fromRaw({ sourceDir: path.join(__dirname, '..', 'server', 'out'), targetDir: path.join(__dirname, 'src-tauri', 'binaries'), baseName: 'server', mappings: [ { bunSuffix: 'windows-x64', tauriTarget: 'x86_64-pc-windows-msvc', }, { bunSuffix: 'darwin-arm64', tauriTarget: 'aarch64-apple-darwin', }, { bunSuffix: 'darwin-x64', tauriTarget: 'x86_64-apple-darwin', }, { bunSuffix: 'linux-x64', tauriTarget: 'x86_64-unknown-linux-gnu', }, { bunSuffix: 'linux-arm64', tauriTarget: 'aarch64-unknown-linux-gnu', }, ], } satisfies CopyConfig), ) } /** * 文件系统服务 */ class FileSystemService extends Context.Tag('FileSystemService')< FileSystemService, { readonly ensureDir: (dir: string) => Effect.Effect readonly fileExists: ( filePath: string, ) => Effect.Effect readonly copyFile: ( source: string, target: string, ) => Effect.Effect } >() { static readonly Live = Layer.succeed(FileSystemService, { ensureDir: (dir: string) => Effect.tryPromise({ try: async () => { await $`mkdir -p ${dir}` }, catch: (cause: unknown) => new FileSystemError({ operation: 'ensureDir', path: dir, cause, }), }), fileExists: (filePath: string) => Effect.tryPromise({ try: async () => { const file = Bun.file(filePath) return await file.exists() }, catch: (cause: unknown) => new FileSystemError({ operation: 'fileExists', path: filePath, cause, }), }), copyFile: (source: string, target: string) => Effect.tryPromise({ try: async () => { await $`cp ${source} ${target}` }, catch: (cause: unknown) => new CopyError({ source, target, cause, }), }), }) } /** * 复制服务 */ class CopyService extends Context.Tag('CopyService')< CopyService, { readonly copyBinary: ( config: CopyConfig, mapping: TargetMapping, ) => Effect.Effect readonly copyAllBinaries: ( config: CopyConfig, ) => Effect.Effect, CopyError | FileSystemError> } >() { static readonly Live = Layer.effect( CopyService, Effect.gen(function* () { const fs = yield* FileSystemService return { copyBinary: (config: CopyConfig, mapping: TargetMapping) => Effect.gen(function* () { const { sourceDir, targetDir, baseName } = config const { bunSuffix, tauriTarget } = mapping // 确定文件扩展名(Windows 需要 .exe) const ext = tauriTarget.includes('windows') ? '.exe' : '' // 构建源文件和目标文件路径 const sourceFile = path.join( sourceDir, `${baseName}-${bunSuffix}${ext}`, ) const targetFile = path.join( targetDir, `${baseName}-${tauriTarget}${ext}`, ) // 检查源文件是否存在 const exists = yield* fs.fileExists(sourceFile) if (!exists) { yield* Console.log(`⚠️ 跳过 ${bunSuffix}: 源文件不存在`) return { bunSuffix, tauriTarget, sourceFile, targetFile, success: false, } satisfies CopyResult } // 复制文件 yield* fs.copyFile(sourceFile, targetFile) yield* Console.log(`✓ ${bunSuffix} → ${tauriTarget}`) yield* Console.log(` ${sourceFile}`) yield* Console.log(` → ${targetFile}\n`) return { bunSuffix, tauriTarget, sourceFile, targetFile, success: true, } satisfies CopyResult }), copyAllBinaries: (config: CopyConfig) => Effect.gen(function* () { const effects = config.mappings.map((mapping) => Effect.gen(function* () { const { sourceDir, targetDir, baseName } = config const { bunSuffix, tauriTarget } = mapping const ext = tauriTarget.includes('windows') ? '.exe' : '' const sourceFile = path.join( sourceDir, `${baseName}-${bunSuffix}${ext}`, ) const targetFile = path.join( targetDir, `${baseName}-${tauriTarget}${ext}`, ) const exists = yield* fs.fileExists(sourceFile) if (!exists) { yield* Console.log(`⚠️ 跳过 ${bunSuffix}: 源文件不存在`) return { bunSuffix, tauriTarget, sourceFile, targetFile, success: false, } satisfies CopyResult } yield* fs.copyFile(sourceFile, targetFile) yield* Console.log(`✓ ${bunSuffix} → ${tauriTarget}`) yield* Console.log(` ${sourceFile}`) yield* Console.log(` → ${targetFile}\n`) return { bunSuffix, tauriTarget, sourceFile, targetFile, success: true, } satisfies CopyResult }), ) return yield* Effect.all(effects, { concurrency: 'unbounded' }) }), } }), ) } /** * 报告服务 */ class ReporterService extends Context.Tag('ReporterService')< ReporterService, { readonly printSummary: ( results: ReadonlyArray, ) => Effect.Effect } >() { static readonly Live = Layer.succeed(ReporterService, { printSummary: (results: ReadonlyArray) => Effect.gen(function* () { const successful = results.filter((r) => r.success) const failed = results.filter((r) => !r.success) yield* Console.log('\n📦 复制摘要:') yield* Console.log(` ✅ 成功: ${successful.length}`) yield* Console.log(` ⚠️ 跳过: ${failed.length}`) if (successful.length > 0) { yield* Console.log('\n成功复制的文件:') for (const result of successful) { yield* Console.log( ` • ${result.bunSuffix} → ${result.tauriTarget}`, ) } } if (failed.length > 0) { yield* Console.log('\n跳过的文件:') for (const result of failed) { yield* Console.log(` • ${result.bunSuffix} (源文件不存在)`) } } }), }) } // ============================================================================ // Main Program // ============================================================================ const program = Effect.gen(function* () { const config = yield* CopyConfigService const fs = yield* FileSystemService const copier = yield* CopyService const reporter = yield* ReporterService yield* Console.log('📦 开始复制二进制文件到 Tauri sidecar 目录...\n') // 1. 检查源目录 const sourceExists = yield* fs.fileExists(config.sourceDir) if (!sourceExists) { yield* Console.error(`❌ 源目录不存在: ${config.sourceDir}`) yield* Console.log( '💡 提示: 请先在 apps/server 中运行 bun run compile 构建服务器二进制文件', ) return yield* Effect.fail( new FileSystemError({ operation: 'checkSourceDir', path: config.sourceDir, cause: '源目录不存在', }), ) } // 2. 创建目标目录 yield* fs.ensureDir(config.targetDir) yield* Console.log(`✓ 目标目录: ${config.targetDir}\n`) // 3. 并行复制所有二进制文件 const results = yield* copier.copyAllBinaries(config) // 4. 输出摘要 yield* reporter.printSummary(results) return results }) // ============================================================================ // Layer Composition // ============================================================================ const MainLayer = Layer.mergeAll( CopyConfigService.Live, FileSystemService.Live, CopyService.Live.pipe(Layer.provide(FileSystemService.Live)), ReporterService.Live, ) // ============================================================================ // Runner // ============================================================================ const runnable = program.pipe( Effect.provide(MainLayer), Effect.catchTags({ ConfigError: (error) => Console.error(`❌ 配置错误: ${error.message}`, error.cause), FileSystemError: (error) => Console.error( `❌ 文件系统错误 [${error.operation}]: ${error.path}`, error.cause, ), CopyError: (error) => Console.error( `❌ 复制失败: ${error.source} → ${error.target}`, error.cause, ), }), Effect.tapErrorCause((cause) => Console.error('❌ 未预期的错误:', cause)), ) Effect.runPromise(runnable).catch(() => { process.exit(1) })