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 {
return 900000; // 15 minutes for all builds
return 900000;
}
private async detectPackageManager(): Promise<string> {
// 使用缓存避免重复检测
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<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) {
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();
}
}

View File

@@ -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}`;
}
}

View File

@@ -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}`;
}
}

5
bin/cli.ts vendored
View File

@@ -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 <string>', 'Installer language')
.default(DEFAULT.installerLanguage)

1
bin/defaults.ts vendored
View File

@@ -26,6 +26,7 @@ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {
incognito: false,
wasm: false,
enableDragDrop: false,
keepBinary: false,
};
// 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
enableDragDrop: boolean;
// Keep raw binary file alongside installer, default false
keepBinary: boolean;
}
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
```
#### [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`.

View File

@@ -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]
设置窗口标题栏文本。如果未指定,窗口标题将为空。