diff --git a/bin/builders/BaseBuilder.ts b/bin/builders/BaseBuilder.ts index c649f67..72b0c51 100644 --- a/bin/builders/BaseBuilder.ts +++ b/bin/builders/BaseBuilder.ts @@ -37,25 +37,22 @@ export default abstract class BaseBuilder { } private getBuildTimeout(): number { - return 900000; // 15 minutes for all builds + return 900000; } private async detectPackageManager(): Promise { - // 使用缓存避免重复检测 if (BaseBuilder.packageManagerCache) { return BaseBuilder.packageManagerCache; } const { execa } = await import('execa'); - // 优先使用pnpm(如果可用) try { await execa('pnpm', ['--version'], { stdio: 'ignore' }); logger.info('✺ Using pnpm for package management.'); BaseBuilder.packageManagerCache = 'pnpm'; return 'pnpm'; } catch { - // pnpm不可用,回退到npm try { await execa('npm', ['--version'], { stdio: 'ignore' }); logger.info('✺ pnpm not available, using npm for package management.'); @@ -176,9 +173,21 @@ export default abstract class BaseBuilder { const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType); const distPath = path.resolve(`${name}.${fileType}`); await fsExtra.copy(appPath, distPath); + + // Copy raw binary if requested + if (this.options.keepBinary) { + await this.copyRawBinary(npmDirectory, name); + } + await fsExtra.remove(appPath); logger.success('✔ Build success!'); logger.success('✔ App installer located in', distPath); + + // Log binary location if preserved + if (this.options.keepBinary) { + const binaryPath = this.getRawBinaryPath(name); + logger.success('✔ Raw binary located in', path.resolve(binaryPath)); + } } protected getFileType(target: string): string { @@ -338,4 +347,86 @@ export default abstract class BaseBuilder { `${fileName}.${fileType}`, ); } + + /** + * Copy raw binary file to output directory + */ + protected async copyRawBinary( + npmDirectory: string, + appName: string, + ): Promise { + const binaryPath = this.getRawBinarySourcePath(npmDirectory, appName); + const outputPath = this.getRawBinaryPath(appName); + + if (await fsExtra.pathExists(binaryPath)) { + await fsExtra.copy(binaryPath, outputPath); + // Make binary executable on Unix-like systems + if (process.platform !== 'win32') { + await fsExtra.chmod(outputPath, 0o755); + } + } else { + logger.warn(`✼ Raw binary not found at ${binaryPath}, skipping...`); + } + } + + /** + * Get the source path of the raw binary file in the build directory + */ + protected getRawBinarySourcePath( + npmDirectory: string, + appName: string, + ): string { + const basePath = this.options.debug ? 'debug' : 'release'; + const binaryName = this.getBinaryName(appName); + + // Handle cross-platform builds + if (this.options.multiArch || this.hasArchSpecificTarget()) { + return path.join( + npmDirectory, + this.getArchSpecificPath(), + basePath, + binaryName, + ); + } + + return path.join(npmDirectory, 'src-tauri/target', basePath, binaryName); + } + + /** + * Get the output path for the raw binary file + */ + protected getRawBinaryPath(appName: string): string { + const extension = process.platform === 'win32' ? '.exe' : ''; + const suffix = process.platform === 'win32' ? '' : '-binary'; + return `${appName}${suffix}${extension}`; + } + + /** + * Get the binary name based on app name and platform + */ + protected getBinaryName(appName: string): string { + const extension = process.platform === 'win32' ? '.exe' : ''; + + // Linux uses the unique binary name we set in merge.ts + if (process.platform === 'linux') { + return `pake-${appName.toLowerCase()}${extension}`; + } + + // Windows and macOS use 'pake' as binary name + return `pake${extension}`; + } + + /** + * Check if this build has architecture-specific target + */ + protected hasArchSpecificTarget(): boolean { + return false; // Override in subclasses if needed + } + + /** + * Get architecture-specific path for binary + */ + protected getArchSpecificPath(): string { + return 'src-tauri/target'; // Override in subclasses if needed + } } diff --git a/bin/builders/LinuxBuilder.ts b/bin/builders/LinuxBuilder.ts index 724cd8e..bc0189b 100644 --- a/bin/builders/LinuxBuilder.ts +++ b/bin/builders/LinuxBuilder.ts @@ -10,7 +10,6 @@ export default class LinuxBuilder extends BaseBuilder { constructor(options: PakeAppOptions) { super(options); - // Parse target format and architecture const target = options.targets || 'deb'; if (target.includes('-arm64')) { this.buildFormat = target.replace('-arm64', ''); @@ -20,7 +19,6 @@ export default class LinuxBuilder extends BaseBuilder { this.buildArch = this.resolveTargetArch('auto'); } - // Set targets to format for Tauri this.options.targets = this.buildFormat; } @@ -28,23 +26,23 @@ export default class LinuxBuilder extends BaseBuilder { const { name, targets } = this.options; const version = tauriConfig.version; - // Determine architecture display name let arch: string; if (this.buildArch === 'arm64') { arch = targets === 'rpm' || targets === 'appimage' ? 'aarch64' : 'arm64'; } else { - // Auto-detect or default to current architecture - const resolvedArch = this.buildArch === 'x64' ? 'amd64' : this.buildArch; - arch = resolvedArch; - if ( - resolvedArch === 'arm64' && - (targets === 'rpm' || targets === 'appimage') - ) { - arch = 'aarch64'; + if (this.buildArch === 'x64') { + arch = targets === 'rpm' ? 'x86_64' : 'amd64'; + } else { + arch = this.buildArch; + if ( + this.buildArch === 'arm64' && + (targets === 'rpm' || targets === 'appimage') + ) { + arch = 'aarch64'; + } } } - // The RPM format uses different separators and version number formats if (targets === 'rpm') { return `${name}-${version}-1.${arch}`; } @@ -52,7 +50,6 @@ export default class LinuxBuilder extends BaseBuilder { return `${name}_${version}_${arch}`; } - // Customize it, considering that there are all targets. async build(url: string) { const targetTypes = ['deb', 'appimage', 'rpm']; for (const target of targetTypes) { @@ -65,7 +62,6 @@ export default class LinuxBuilder extends BaseBuilder { protected getBuildCommand(packageManager: string = 'pnpm'): string { const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json'); - // Only add target if it's ARM64 const buildTarget = this.buildArch === 'arm64' ? this.getTauriTarget(this.buildArch, 'linux') @@ -77,7 +73,6 @@ export default class LinuxBuilder extends BaseBuilder { buildTarget, ); - // Add features const features = this.getBuildFeatures(); if (features.length > 0) { fullCommand += ` --features ${features.join(',')}`; @@ -103,4 +98,16 @@ export default class LinuxBuilder extends BaseBuilder { } return super.getFileType(target); } + + protected hasArchSpecificTarget(): boolean { + return this.buildArch === 'arm64'; + } + + protected getArchSpecificPath(): string { + if (this.buildArch === 'arm64') { + const target = this.getTauriTarget(this.buildArch, 'linux'); + return `src-tauri/target/${target}`; + } + return super.getArchSpecificPath(); + } } diff --git a/bin/builders/MacBuilder.ts b/bin/builders/MacBuilder.ts index 969bc13..0ef7c92 100644 --- a/bin/builders/MacBuilder.ts +++ b/bin/builders/MacBuilder.ts @@ -10,35 +10,27 @@ export default class MacBuilder extends BaseBuilder { constructor(options: PakeAppOptions) { super(options); - // Store the original targets value for architecture selection - // For macOS, targets can be architecture names or format names - // Filter out non-architecture values const validArchs = ['intel', 'apple', 'universal', 'auto', 'x64', 'arm64']; this.buildArch = validArchs.includes(options.targets || '') ? options.targets : 'auto'; - // Use DMG by default for distribution - // Only create app bundles for testing to avoid user interaction if (process.env.PAKE_CREATE_APP === '1') { this.buildFormat = 'app'; } else { this.buildFormat = 'dmg'; } - // Set targets to format for Tauri this.options.targets = this.buildFormat; } getFileName(): string { const { name } = this.options; - // For app bundles, use simple name without version/arch if (this.buildFormat === 'app') { return name; } - // For DMG files, use versioned filename let arch: string; if (this.buildArch === 'universal' || this.options.multiArch) { arch = 'universal'; @@ -47,7 +39,6 @@ export default class MacBuilder extends BaseBuilder { } else if (this.buildArch === 'intel') { arch = 'x64'; } else { - // Auto-detect based on current architecture arch = this.getArchDisplayName(this.resolveTargetArch(this.buildArch)); } return `${name}_${tauriConfig.version}_${arch}`; @@ -79,7 +70,6 @@ export default class MacBuilder extends BaseBuilder { buildTarget, ); - // Add features const features = this.getBuildFeatures(); if (features.length > 0) { fullCommand += ` --features ${features.join(',')}`; @@ -95,4 +85,14 @@ export default class MacBuilder extends BaseBuilder { return `src-tauri/target/${target}/${basePath}/bundle`; } + + protected hasArchSpecificTarget(): boolean { + return true; + } + + protected getArchSpecificPath(): string { + const actualArch = this.getActualArch(); + const target = this.getTauriTarget(actualArch, 'darwin'); + return `src-tauri/target/${target}`; + } } diff --git a/bin/builders/WinBuilder.ts b/bin/builders/WinBuilder.ts index a7c0eaa..5d3b8e7 100644 --- a/bin/builders/WinBuilder.ts +++ b/bin/builders/WinBuilder.ts @@ -9,8 +9,6 @@ export default class WinBuilder extends BaseBuilder { constructor(options: PakeAppOptions) { super(options); - // For Windows, targets can be architecture names or format names - // Filter out non-architecture values const validArchs = ['x64', 'arm64', 'auto']; this.buildArch = validArchs.includes(options.targets || '') ? this.resolveTargetArch(options.targets) @@ -41,7 +39,6 @@ export default class WinBuilder extends BaseBuilder { buildTarget, ); - // Add features const features = this.getBuildFeatures(); if (features.length > 0) { fullCommand += ` --features ${features.join(',')}`; @@ -55,4 +52,13 @@ export default class WinBuilder extends BaseBuilder { const target = this.getTauriTarget(this.buildArch, 'win32'); return `src-tauri/target/${target}/${basePath}/bundle/`; } + + protected hasArchSpecificTarget(): boolean { + return true; + } + + protected getArchSpecificPath(): string { + const target = this.getTauriTarget(this.buildArch, 'win32'); + return `src-tauri/target/${target}`; + } } diff --git a/bin/cli.ts b/bin/cli.ts index 12cecbd..8213e49 100644 --- a/bin/cli.ts +++ b/bin/cli.ts @@ -148,6 +148,11 @@ program .default(DEFAULT.enableDragDrop) .hideHelp(), ) + .addOption( + new Option('--keep-binary', 'Keep raw binary file alongside installer') + .default(DEFAULT.keepBinary) + .hideHelp(), + ) .addOption( new Option('--installer-language ', 'Installer language') .default(DEFAULT.installerLanguage) diff --git a/bin/defaults.ts b/bin/defaults.ts index 72ab520..caa7b2e 100644 --- a/bin/defaults.ts +++ b/bin/defaults.ts @@ -26,6 +26,7 @@ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = { incognito: false, wasm: false, enableDragDrop: false, + keepBinary: false, }; // Just for cli development diff --git a/bin/types.ts b/bin/types.ts index 261c6b2..9241a31 100644 --- a/bin/types.ts +++ b/bin/types.ts @@ -84,6 +84,9 @@ export interface PakeCliOptions { // Enable drag and drop functionality, default false enableDragDrop: boolean; + + // Keep raw binary file alongside installer, default false + keepBinary: boolean; } export interface PakeAppOptions extends PakeCliOptions { diff --git a/docs/cli-usage.md b/docs/cli-usage.md index 7a6ccfd..1e75848 100644 --- a/docs/cli-usage.md +++ b/docs/cli-usage.md @@ -347,6 +347,19 @@ Enable native drag and drop functionality within the application. Default is `fa pake https://planka.example.com --name PlankApp --enable-drag-drop ``` +#### [keep-binary] + +Keep the raw binary file alongside the installer. Default is `false`. When enabled, also outputs a standalone executable that can run without installation. + +```shell +--keep-binary + +# Example: Package app with both installer and standalone binary +pake https://github.com --name GitHub --keep-binary +``` + +**Output**: Creates both installer and standalone executable (`AppName-binary` on Unix, `AppName.exe` on Windows). + #### [installer-language] Set the Windows Installer language. Options include `zh-CN`, `ja-JP`, More at [Tauri Document](https://tauri.app/distribute/windows-installer/#internationalization). Default is `en-US`. diff --git a/docs/cli-usage_CN.md b/docs/cli-usage_CN.md index 68f7258..c9832d4 100644 --- a/docs/cli-usage_CN.md +++ b/docs/cli-usage_CN.md @@ -334,6 +334,19 @@ pake https://flutter.dev --name FlutterApp --wasm pake https://planka.example.com --name PlankApp --enable-drag-drop ``` +#### [keep-binary] + +保留原始二进制文件与安装包一起。默认为 `false`。启用后,除了平台特定的安装包外,还会输出一个可独立运行的可执行文件。 + +```shell +--keep-binary + +# 示例:同时生成安装包和独立可执行文件 +pake https://github.com --name GitHub --keep-binary +``` + +**输出结果**:同时创建安装包和独立可执行文件(Unix 系统为 `AppName-binary`,Windows 为 `AppName.exe`)。 + #### [title] 设置窗口标题栏文本。如果未指定,窗口标题将为空。