@@ -0,0 +1,452 @@
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 < typeof BunTargetSuffixSchema >
/**
* 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 < typeof TauriTargetSchema >
/**
* 目标映射配置
*/
const TargetMappingSchema = Schema . Struct ( {
bunSuffix : BunTargetSuffixSchema ,
tauriTarget : TauriTargetSchema ,
} )
type TargetMapping = Schema . Schema . Type < typeof TargetMappingSchema >
/**
* 复制配置
*/
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 < typeof CopyConfigSchema >
/**
* 复制结果
*/
const CopyResultSchema = Schema . Struct ( {
bunSuffix : BunTargetSuffixSchema ,
tauriTarget : TauriTargetSchema ,
sourceFile : Schema.String ,
targetFile : Schema.String ,
success : Schema.Boolean ,
} )
type CopyResult = Schema . Schema . Type < typeof CopyResultSchema >
// ============================================================================
// 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 < void , FileSystemError >
readonly fileExists : (
filePath : string ,
) = > Effect . Effect < boolean , FileSystemError >
readonly copyFile : (
source : string ,
target : string ,
) = > Effect . Effect < void , CopyError >
}
> ( ) {
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 < CopyResult , CopyError | FileSystemError >
readonly copyAllBinaries : (
config : CopyConfig ,
) = > Effect . Effect < ReadonlyArray < CopyResult > , 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 < CopyResult > ,
) = > Effect . Effect < void >
}
> ( ) {
static readonly Live = Layer . succeed ( ReporterService , {
printSummary : ( results : ReadonlyArray < CopyResult > ) = >
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 )
} )