Support keep-binary usage

This commit is contained in:
Tw93
2025-09-05 15:14:03 +08:00
parent a30e09d3e4
commit 14f337fe2b
9 changed files with 171 additions and 32 deletions

View File

@@ -37,25 +37,22 @@ export default abstract class BaseBuilder {
} }
private getBuildTimeout(): number { private getBuildTimeout(): number {
return 900000; // 15 minutes for all builds return 900000;
} }
private async detectPackageManager(): Promise<string> { private async detectPackageManager(): Promise<string> {
// 使用缓存避免重复检测
if (BaseBuilder.packageManagerCache) { if (BaseBuilder.packageManagerCache) {
return BaseBuilder.packageManagerCache; return BaseBuilder.packageManagerCache;
} }
const { execa } = await import('execa'); const { execa } = await import('execa');
// 优先使用pnpm如果可用
try { try {
await execa('pnpm', ['--version'], { stdio: 'ignore' }); await execa('pnpm', ['--version'], { stdio: 'ignore' });
logger.info('✺ Using pnpm for package management.'); logger.info('✺ Using pnpm for package management.');
BaseBuilder.packageManagerCache = 'pnpm'; BaseBuilder.packageManagerCache = 'pnpm';
return 'pnpm'; return 'pnpm';
} catch { } catch {
// pnpm不可用回退到npm
try { try {
await execa('npm', ['--version'], { stdio: 'ignore' }); await execa('npm', ['--version'], { stdio: 'ignore' });
logger.info('✺ pnpm not available, using npm for package management.'); 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 appPath = this.getBuildAppPath(npmDirectory, fileName, fileType);
const distPath = path.resolve(`${name}.${fileType}`); const distPath = path.resolve(`${name}.${fileType}`);
await fsExtra.copy(appPath, distPath); await fsExtra.copy(appPath, distPath);
// Copy raw binary if requested
if (this.options.keepBinary) {
await this.copyRawBinary(npmDirectory, name);
}
await fsExtra.remove(appPath); await fsExtra.remove(appPath);
logger.success('✔ Build success!'); logger.success('✔ Build success!');
logger.success('✔ App installer located in', distPath); 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 { protected getFileType(target: string): string {
@@ -338,4 +347,86 @@ export default abstract class BaseBuilder {
`${fileName}.${fileType}`, `${fileName}.${fileType}`,
); );
} }
/**
* Copy raw binary file to output directory
*/
protected async copyRawBinary(
npmDirectory: string,
appName: string,
): Promise<void> {
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
}
} }

View File

@@ -10,7 +10,6 @@ export default class LinuxBuilder extends BaseBuilder {
constructor(options: PakeAppOptions) { constructor(options: PakeAppOptions) {
super(options); super(options);
// Parse target format and architecture
const target = options.targets || 'deb'; const target = options.targets || 'deb';
if (target.includes('-arm64')) { if (target.includes('-arm64')) {
this.buildFormat = target.replace('-arm64', ''); this.buildFormat = target.replace('-arm64', '');
@@ -20,7 +19,6 @@ export default class LinuxBuilder extends BaseBuilder {
this.buildArch = this.resolveTargetArch('auto'); this.buildArch = this.resolveTargetArch('auto');
} }
// Set targets to format for Tauri
this.options.targets = this.buildFormat; this.options.targets = this.buildFormat;
} }
@@ -28,23 +26,23 @@ export default class LinuxBuilder extends BaseBuilder {
const { name, targets } = this.options; const { name, targets } = this.options;
const version = tauriConfig.version; const version = tauriConfig.version;
// Determine architecture display name
let arch: string; let arch: string;
if (this.buildArch === 'arm64') { if (this.buildArch === 'arm64') {
arch = targets === 'rpm' || targets === 'appimage' ? 'aarch64' : 'arm64'; arch = targets === 'rpm' || targets === 'appimage' ? 'aarch64' : 'arm64';
} else { } else {
// Auto-detect or default to current architecture if (this.buildArch === 'x64') {
const resolvedArch = this.buildArch === 'x64' ? 'amd64' : this.buildArch; arch = targets === 'rpm' ? 'x86_64' : 'amd64';
arch = resolvedArch; } else {
if ( arch = this.buildArch;
resolvedArch === 'arm64' && if (
(targets === 'rpm' || targets === 'appimage') this.buildArch === 'arm64' &&
) { (targets === 'rpm' || targets === 'appimage')
arch = 'aarch64'; ) {
arch = 'aarch64';
}
} }
} }
// The RPM format uses different separators and version number formats
if (targets === 'rpm') { if (targets === 'rpm') {
return `${name}-${version}-1.${arch}`; return `${name}-${version}-1.${arch}`;
} }
@@ -52,7 +50,6 @@ export default class LinuxBuilder extends BaseBuilder {
return `${name}_${version}_${arch}`; return `${name}_${version}_${arch}`;
} }
// Customize it, considering that there are all targets.
async build(url: string) { async build(url: string) {
const targetTypes = ['deb', 'appimage', 'rpm']; const targetTypes = ['deb', 'appimage', 'rpm'];
for (const target of targetTypes) { for (const target of targetTypes) {
@@ -65,7 +62,6 @@ export default class LinuxBuilder extends BaseBuilder {
protected getBuildCommand(packageManager: string = 'pnpm'): string { protected getBuildCommand(packageManager: string = 'pnpm'): string {
const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json'); const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
// Only add target if it's ARM64
const buildTarget = const buildTarget =
this.buildArch === 'arm64' this.buildArch === 'arm64'
? this.getTauriTarget(this.buildArch, 'linux') ? this.getTauriTarget(this.buildArch, 'linux')
@@ -77,7 +73,6 @@ export default class LinuxBuilder extends BaseBuilder {
buildTarget, buildTarget,
); );
// Add features
const features = this.getBuildFeatures(); const features = this.getBuildFeatures();
if (features.length > 0) { if (features.length > 0) {
fullCommand += ` --features ${features.join(',')}`; fullCommand += ` --features ${features.join(',')}`;
@@ -103,4 +98,16 @@ export default class LinuxBuilder extends BaseBuilder {
} }
return super.getFileType(target); 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();
}
} }

View File

@@ -10,35 +10,27 @@ export default class MacBuilder extends BaseBuilder {
constructor(options: PakeAppOptions) { constructor(options: PakeAppOptions) {
super(options); 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']; const validArchs = ['intel', 'apple', 'universal', 'auto', 'x64', 'arm64'];
this.buildArch = validArchs.includes(options.targets || '') this.buildArch = validArchs.includes(options.targets || '')
? options.targets ? options.targets
: 'auto'; : 'auto';
// Use DMG by default for distribution
// Only create app bundles for testing to avoid user interaction
if (process.env.PAKE_CREATE_APP === '1') { if (process.env.PAKE_CREATE_APP === '1') {
this.buildFormat = 'app'; this.buildFormat = 'app';
} else { } else {
this.buildFormat = 'dmg'; this.buildFormat = 'dmg';
} }
// Set targets to format for Tauri
this.options.targets = this.buildFormat; this.options.targets = this.buildFormat;
} }
getFileName(): string { getFileName(): string {
const { name } = this.options; const { name } = this.options;
// For app bundles, use simple name without version/arch
if (this.buildFormat === 'app') { if (this.buildFormat === 'app') {
return name; return name;
} }
// For DMG files, use versioned filename
let arch: string; let arch: string;
if (this.buildArch === 'universal' || this.options.multiArch) { if (this.buildArch === 'universal' || this.options.multiArch) {
arch = 'universal'; arch = 'universal';
@@ -47,7 +39,6 @@ export default class MacBuilder extends BaseBuilder {
} else if (this.buildArch === 'intel') { } else if (this.buildArch === 'intel') {
arch = 'x64'; arch = 'x64';
} else { } else {
// Auto-detect based on current architecture
arch = this.getArchDisplayName(this.resolveTargetArch(this.buildArch)); arch = this.getArchDisplayName(this.resolveTargetArch(this.buildArch));
} }
return `${name}_${tauriConfig.version}_${arch}`; return `${name}_${tauriConfig.version}_${arch}`;
@@ -79,7 +70,6 @@ export default class MacBuilder extends BaseBuilder {
buildTarget, buildTarget,
); );
// Add features
const features = this.getBuildFeatures(); const features = this.getBuildFeatures();
if (features.length > 0) { if (features.length > 0) {
fullCommand += ` --features ${features.join(',')}`; fullCommand += ` --features ${features.join(',')}`;
@@ -95,4 +85,14 @@ export default class MacBuilder extends BaseBuilder {
return `src-tauri/target/${target}/${basePath}/bundle`; 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}`;
}
} }

View File

@@ -9,8 +9,6 @@ export default class WinBuilder extends BaseBuilder {
constructor(options: PakeAppOptions) { constructor(options: PakeAppOptions) {
super(options); super(options);
// For Windows, targets can be architecture names or format names
// Filter out non-architecture values
const validArchs = ['x64', 'arm64', 'auto']; const validArchs = ['x64', 'arm64', 'auto'];
this.buildArch = validArchs.includes(options.targets || '') this.buildArch = validArchs.includes(options.targets || '')
? this.resolveTargetArch(options.targets) ? this.resolveTargetArch(options.targets)
@@ -41,7 +39,6 @@ export default class WinBuilder extends BaseBuilder {
buildTarget, buildTarget,
); );
// Add features
const features = this.getBuildFeatures(); const features = this.getBuildFeatures();
if (features.length > 0) { if (features.length > 0) {
fullCommand += ` --features ${features.join(',')}`; fullCommand += ` --features ${features.join(',')}`;
@@ -55,4 +52,13 @@ export default class WinBuilder extends BaseBuilder {
const target = this.getTauriTarget(this.buildArch, 'win32'); const target = this.getTauriTarget(this.buildArch, 'win32');
return `src-tauri/target/${target}/${basePath}/bundle/`; 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}`;
}
} }

5
bin/cli.ts vendored
View File

@@ -148,6 +148,11 @@ program
.default(DEFAULT.enableDragDrop) .default(DEFAULT.enableDragDrop)
.hideHelp(), .hideHelp(),
) )
.addOption(
new Option('--keep-binary', 'Keep raw binary file alongside installer')
.default(DEFAULT.keepBinary)
.hideHelp(),
)
.addOption( .addOption(
new Option('--installer-language <string>', 'Installer language') new Option('--installer-language <string>', 'Installer language')
.default(DEFAULT.installerLanguage) .default(DEFAULT.installerLanguage)

1
bin/defaults.ts vendored
View File

@@ -26,6 +26,7 @@ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {
incognito: false, incognito: false,
wasm: false, wasm: false,
enableDragDrop: false, enableDragDrop: false,
keepBinary: false,
}; };
// Just for cli development // Just for cli development

3
bin/types.ts vendored
View File

@@ -84,6 +84,9 @@ export interface PakeCliOptions {
// Enable drag and drop functionality, default false // Enable drag and drop functionality, default false
enableDragDrop: boolean; enableDragDrop: boolean;
// Keep raw binary file alongside installer, default false
keepBinary: boolean;
} }
export interface PakeAppOptions extends PakeCliOptions { export interface PakeAppOptions extends PakeCliOptions {

View File

@@ -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 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] #### [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`. 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`.

View File

@@ -334,6 +334,19 @@ pake https://flutter.dev --name FlutterApp --wasm
pake https://planka.example.com --name PlankApp --enable-drag-drop 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] #### [title]
设置窗口标题栏文本。如果未指定,窗口标题将为空。 设置窗口标题栏文本。如果未指定,窗口标题将为空。