diff --git a/bin/builders/BaseBuilder.ts b/bin/builders/BaseBuilder.ts index 0d71fc0..18d4d5e 100644 --- a/bin/builders/BaseBuilder.ts +++ b/bin/builders/BaseBuilder.ts @@ -162,11 +162,15 @@ export default abstract class BaseBuilder { // Show static message to keep the status visible logger.warn('✸ Building app...'); - const buildEnv = { - ...this.getBuildEnvironment(), - ...(process.env.NO_STRIP && { NO_STRIP: process.env.NO_STRIP }), + const baseEnv = this.getBuildEnvironment(); + let buildEnv: Record = { + ...(baseEnv ?? {}), + ...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}), }; + const resolveExecEnv = () => + Object.keys(buildEnv).length > 0 ? buildEnv : undefined; + // Warn users about potential AppImage build failures on modern Linux systems. // The linuxdeploy tool bundled in Tauri uses an older strip tool that doesn't // recognize the .relr.dyn section introduced in glibc 2.38+. @@ -181,11 +185,31 @@ export default abstract class BaseBuilder { } } - await shellExec( - `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`, - this.getBuildTimeout(), - buildEnv, - ); + const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`; + const buildTimeout = this.getBuildTimeout(); + + try { + await shellExec(buildCommand, buildTimeout, resolveExecEnv()); + } catch (error) { + const shouldRetryWithoutStrip = + process.platform === 'linux' && + this.options.targets === 'appimage' && + !buildEnv.NO_STRIP && + this.isLinuxDeployStripError(error); + + if (shouldRetryWithoutStrip) { + logger.warn( + '⚠ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.', + ); + buildEnv = { + ...buildEnv, + NO_STRIP: '1', + }; + await shellExec(buildCommand, buildTimeout, resolveExecEnv()); + } else { + throw error; + } + } // Copy app const fileName = this.getFileName(); @@ -216,6 +240,21 @@ export default abstract class BaseBuilder { abstract getFileName(): string; + private isLinuxDeployStripError(error: unknown): boolean { + if (!(error instanceof Error) || !error.message) { + return false; + } + const message = error.message.toLowerCase(); + return ( + message.includes('linuxdeploy') || + message.includes('failed to run linuxdeploy') || + message.includes('strip:') || + message.includes('unable to recognise the format of the input file') || + message.includes('appimage tool failed') || + message.includes('strip tool') + ); + } + // 架构映射配置 protected static readonly ARCH_MAPPINGS: Record< string, diff --git a/bin/utils/shell.ts b/bin/utils/shell.ts index 7c14980..e857d8f 100644 --- a/bin/utils/shell.ts +++ b/bin/utils/shell.ts @@ -31,11 +31,13 @@ export async function shellExec( // Provide helpful guidance for common Linux AppImage build failures // caused by strip tool incompatibility with modern glibc (2.38+) + const lowerError = errorMessage.toLowerCase(); + if ( process.platform === 'linux' && - (errorMessage.includes('linuxdeploy') || - errorMessage.includes('appimage') || - errorMessage.includes('strip')) + (lowerError.includes('linuxdeploy') || + lowerError.includes('appimage') || + lowerError.includes('strip')) ) { errorMsg += '\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' + @@ -50,6 +52,18 @@ export async function shellExec( ' • Update binutils: sudo apt install binutils (or pacman -S binutils)\n' + ' • Detailed guide: https://github.com/tw93/Pake/blob/main/docs/faq.md\n' + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; + + if ( + lowerError.includes('fuse') || + lowerError.includes('operation not permitted') || + lowerError.includes('/dev/fuse') + ) { + errorMsg += + '\n\nDocker / Container hint:\n' + + ' AppImage tooling needs access to /dev/fuse. When running inside Docker, add:\n' + + ' --privileged --device /dev/fuse --security-opt apparmor=unconfined\n' + + ' or run on the host directly.'; + } } throw new Error(errorMsg); diff --git a/dist/cli.js b/dist/cli.js index 3be1727..76caf3d 100755 --- a/dist/cli.js +++ b/dist/cli.js @@ -23,7 +23,7 @@ import sharp from 'sharp'; import * as psl from 'psl'; var name = "pake-cli"; -var version = "3.4.1"; +var version = "3.4.2"; var description = "🤱🏻 Turn any webpage into a desktop app with one command. 🤱🏻 一键打包网页生成轻量桌面应用。"; var engines = { node: ">=18.0.0" @@ -223,10 +223,11 @@ async function shellExec(command, timeout = 300000, env) { let errorMsg = `Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`; // Provide helpful guidance for common Linux AppImage build failures // caused by strip tool incompatibility with modern glibc (2.38+) + const lowerError = errorMessage.toLowerCase(); if (process.platform === 'linux' && - (errorMessage.includes('linuxdeploy') || - errorMessage.includes('appimage') || - errorMessage.includes('strip'))) { + (lowerError.includes('linuxdeploy') || + lowerError.includes('appimage') || + lowerError.includes('strip'))) { errorMsg += '\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' + 'Linux AppImage Build Failed\n' + @@ -240,6 +241,15 @@ async function shellExec(command, timeout = 300000, env) { ' • Update binutils: sudo apt install binutils (or pacman -S binutils)\n' + ' • Detailed guide: https://github.com/tw93/Pake/blob/main/docs/faq.md\n' + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; + if (lowerError.includes('fuse') || + lowerError.includes('operation not permitted') || + lowerError.includes('/dev/fuse')) { + errorMsg += + '\n\nDocker / Container hint:\n' + + ' AppImage tooling needs access to /dev/fuse. When running inside Docker, add:\n' + + ' --privileged --device /dev/fuse --security-opt apparmor=unconfined\n' + + ' or run on the host directly.'; + } } throw new Error(errorMsg); } @@ -816,10 +826,12 @@ class BaseBuilder { buildSpinner.stop(); // Show static message to keep the status visible logger.warn('✸ Building app...'); - const buildEnv = { - ...this.getBuildEnvironment(), - ...(process.env.NO_STRIP && { NO_STRIP: process.env.NO_STRIP }), + const baseEnv = this.getBuildEnvironment(); + let buildEnv = { + ...(baseEnv ?? {}), + ...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}), }; + const resolveExecEnv = () => Object.keys(buildEnv).length > 0 ? buildEnv : undefined; // Warn users about potential AppImage build failures on modern Linux systems. // The linuxdeploy tool bundled in Tauri uses an older strip tool that doesn't // recognize the .relr.dyn section introduced in glibc 2.38+. @@ -829,7 +841,28 @@ class BaseBuilder { logger.warn('⚠ If build fails, retry with: NO_STRIP=1 pake --targets appimage'); } } - await shellExec(`cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`, this.getBuildTimeout(), buildEnv); + const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`; + const buildTimeout = this.getBuildTimeout(); + try { + await shellExec(buildCommand, buildTimeout, resolveExecEnv()); + } + catch (error) { + const shouldRetryWithoutStrip = process.platform === 'linux' && + this.options.targets === 'appimage' && + !buildEnv.NO_STRIP && + this.isLinuxDeployStripError(error); + if (shouldRetryWithoutStrip) { + logger.warn('⚠ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.'); + buildEnv = { + ...buildEnv, + NO_STRIP: '1', + }; + await shellExec(buildCommand, buildTimeout, resolveExecEnv()); + } + else { + throw error; + } + } // Copy app const fileName = this.getFileName(); const fileType = this.getFileType(target); @@ -852,6 +885,18 @@ class BaseBuilder { getFileType(target) { return target; } + isLinuxDeployStripError(error) { + if (!(error instanceof Error) || !error.message) { + return false; + } + const message = error.message.toLowerCase(); + return (message.includes('linuxdeploy') || + message.includes('failed to run linuxdeploy') || + message.includes('strip:') || + message.includes('unable to recognise the format of the input file') || + message.includes('appimage tool failed') || + message.includes('strip tool')); + } /** * 解析目标架构 */ @@ -1209,7 +1254,8 @@ class LinuxBuilder extends BaseBuilder { // Enable verbose output for AppImage builds when debugging or PAKE_VERBOSE is set. // AppImage builds often fail with minimal error messages from linuxdeploy, // so verbose mode helps diagnose issues like strip failures and missing dependencies. - if (this.options.targets === 'appimage' && (this.options.debug || process.env.PAKE_VERBOSE)) { + if (this.options.targets === 'appimage' && + (this.options.debug || process.env.PAKE_VERBOSE)) { fullCommand += ' --verbose'; } return fullCommand; diff --git a/docs/cli-usage.md b/docs/cli-usage.md index 63db433..c003a8c 100644 --- a/docs/cli-usage.md +++ b/docs/cli-usage.md @@ -438,16 +438,19 @@ After completing the above steps, your application should be successfully packag ## Docker ```shell -# On Linux, you can run the Pake CLI via Docker -docker run -it --rm \ # Run interactively, remove container after exit - -v YOUR_DIR:/output \ # Files from container's /output will be in YOU_DIR +# Run the Pake CLI via Docker (AppImage builds need FUSE access) +docker run --rm --privileged \ + --device /dev/fuse \ + --security-opt apparmor=unconfined \ + -v YOUR_DIR:/output \ ghcr.io/tw93/pake \ # For example: -docker run -it --rm \ +docker run --rm --privileged \ + --device /dev/fuse \ + --security-opt apparmor=unconfined \ -v ./packages:/output \ ghcr.io/tw93/pake \ - https://example.com --name myapp --icon ./icon.png - + https://example.com --name myapp --icon ./icon.png --targets appimage ``` diff --git a/docs/cli-usage_CN.md b/docs/cli-usage_CN.md index a140731..6ac2d6b 100644 --- a/docs/cli-usage_CN.md +++ b/docs/cli-usage_CN.md @@ -436,16 +436,19 @@ pake ./my-app/index.html --name "my-app" --use-local-file ## Docker 使用 ```shell -# 在Linux上,您可以通过 Docker 运行 Pake CLI。 -docker run -it --rm \ # Run interactively, remove container after exit - -v YOUR_DIR:/output \ # Files from container's /output will be in YOU_DIR +# 在 Linux 上通过 Docker 运行 Pake CLI(AppImage 构建需要 FUSE 权限) +docker run --rm --privileged \ + --device /dev/fuse \ + --security-opt apparmor=unconfined \ + -v YOUR_DIR:/output \ ghcr.io/tw93/pake \ -# For example: -docker run -it --rm \ +# 例如: +docker run --rm --privileged \ + --device /dev/fuse \ + --security-opt apparmor=unconfined \ -v ./packages:/output \ ghcr.io/tw93/pake \ - https://example.com --name MyApp --icon ./icon.png - + https://example.com --name MyApp --icon ./icon.png --targets appimage ``` diff --git a/docs/faq.md b/docs/faq.md index ce1cad1..2c8cfb2 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -16,12 +16,12 @@ Error: failed to run linuxdeploy Error: strip: Unable to recognise the format of the input file ``` -**Solution 1: Use NO_STRIP (Recommended)** +**Solution 1: Automatic NO_STRIP Retry (Recommended)** -Simply add `NO_STRIP=true` before your build command: +Pake CLI now automatically retries AppImage builds with `NO_STRIP=1` when linuxdeploy fails to strip the binary. To skip the strip step from the very first attempt (or when scripting your own builds), set the variable manually: ```bash -NO_STRIP=true pake https://example.com --name MyApp --targets appimage +NO_STRIP=1 pake https://example.com --name MyApp --targets appimage ``` This bypasses the library stripping process that often causes issues on certain Linux distributions. @@ -50,7 +50,7 @@ sudo apt install -y \ pkg-config ``` -Then try building again with `NO_STRIP=true`. +Then try building again (you can still pre-set `NO_STRIP=1` if you prefer). **Solution 3: Use DEB Format Instead** @@ -60,16 +60,21 @@ DEB packages are more stable on Debian-based systems: pake https://example.com --name MyApp --targets deb ``` -**Solution 4: Use Docker** +**Solution 4: Use Docker (with FUSE access)** -Build in a clean environment without installing dependencies: +Build in a clean environment without installing dependencies. AppImage tooling needs access to `/dev/fuse`, so run the container in privileged mode (or grant FUSE explicitly): ```bash -docker run --rm -v $(pwd)/output:/app/output \ +docker run --rm --privileged \ + --device /dev/fuse \ + --security-opt apparmor=unconfined \ + -v $(pwd)/output:/output \ ghcr.io/tw93/pake:latest \ - pake https://example.com --name MyApp --targets appimage + https://example.com --name MyApp --targets appimage ``` +> **Tip:** The generated AppImage may be owned by root. Run `sudo chown $(id -nu):$(id -ng) ./output/MyApp.AppImage` afterwards. + **Why This Happens:** This is a known issue with Tauri's linuxdeploy tool, which can fail when: @@ -78,7 +83,7 @@ This is a known issue with Tauri's linuxdeploy tool, which can fail when: - Building on newer distributions (Arch, Debian Trixie, etc.) - Missing WebKit2GTK or GTK development libraries -The `NO_STRIP=true` environment variable is the official workaround recommended by the Tauri community. +The `NO_STRIP=1` environment variable is the official workaround recommended by the Tauri community. --- diff --git a/docs/faq_CN.md b/docs/faq_CN.md index e5606c9..d4feabc 100644 --- a/docs/faq_CN.md +++ b/docs/faq_CN.md @@ -16,12 +16,12 @@ Error: failed to run linuxdeploy Error: strip: Unable to recognise the format of the input file ``` -**解决方案 1:使用 NO_STRIP(推荐)** +**解决方案 1:自动 NO_STRIP 重试(推荐)** -在构建命令前加上 `NO_STRIP=true`: +Pake CLI 已在 linuxdeploy 剥离失败时自动使用 `NO_STRIP=1` 进行二次构建。如果你希望一开始就跳过剥离步骤(或在脚本中使用),可以手动设置该变量: ```bash -NO_STRIP=true pake https://example.com --name MyApp --targets appimage +NO_STRIP=1 pake https://example.com --name MyApp --targets appimage ``` 这会绕过经常在某些 Linux 发行版上出现问题的库文件剥离过程。 @@ -50,7 +50,7 @@ sudo apt install -y \ pkg-config ``` -然后使用 `NO_STRIP=true` 再次尝试构建。 +然后再次尝试构建(也可以提前设置 `NO_STRIP=1`)。 **解决方案 3:改用 DEB 格式** @@ -60,16 +60,21 @@ DEB 包在基于 Debian 的系统上更稳定: pake https://example.com --name MyApp --targets deb ``` -**解决方案 4:使用 Docker** +**解决方案 4:使用 Docker(需开放 FUSE)** -在干净的环境中构建,无需安装依赖: +在干净的环境中构建,无需安装依赖。AppImage 工具需要访问 `/dev/fuse`,因此需要以特权模式运行(或显式授权 FUSE): ```bash -docker run --rm -v $(pwd)/output:/app/output \ +docker run --rm --privileged \ + --device /dev/fuse \ + --security-opt apparmor=unconfined \ + -v $(pwd)/output:/output \ ghcr.io/tw93/pake:latest \ - pake https://example.com --name MyApp --targets appimage + https://example.com --name MyApp --targets appimage ``` +> **提示:** 生成的 AppImage 可能属于 root,需要执行 `sudo chown $(id -nu):$(id -ng) ./output/MyApp.AppImage` 调整所有权。 + **原因:** 这是 Tauri 的 linuxdeploy 工具的已知问题,在以下情况下可能失败: @@ -78,7 +83,7 @@ docker run --rm -v $(pwd)/output:/app/output \ - 在较新的发行版上构建(Arch、Debian Trixie 等) - 缺少 WebKit2GTK 或 GTK 开发库 -`NO_STRIP=true` 环境变量是 Tauri 社区推荐的官方解决方法。 +`NO_STRIP=1` 环境变量是 Tauri 社区推荐的官方解决方法。 --- diff --git a/package.json b/package.json index db709ca..852f473 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pake-cli", - "version": "3.4.1", + "version": "3.4.2", "description": "🤱🏻 Turn any webpage into a desktop app with one command. 🤱🏻 一键打包网页生成轻量桌面应用。", "engines": { "node": ">=18.0.0" diff --git a/src-tauri/src/app/window.rs b/src-tauri/src/app/window.rs index 9933b17..373357b 100644 --- a/src-tauri/src/app/window.rs +++ b/src-tauri/src/app/window.rs @@ -6,6 +6,22 @@ use tauri::{App, Config, Url, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; #[cfg(target_os = "macos")] use tauri::{Theme, TitleBarStyle}; +#[cfg(target_os = "windows")] +fn build_proxy_browser_arg(url: &Url) -> Option { + let host = url.host_str()?; + let scheme = url.scheme(); + let port = url.port().or_else(|| match scheme { + "http" => Some(80), + "socks5" => Some(1080), + _ => None, + })?; + + match scheme { + "http" | "socks5" => Some(format!("--proxy-server={scheme}://{host}:{port}")), + _ => None, + } +} + pub fn set_window(app: &mut App, config: &PakeConfig, tauri_config: &Config) -> WebviewWindow { let package_name = tauri_config.clone().product_name.unwrap(); let _data_dir = get_data_dir(app.handle(), package_name); @@ -60,12 +76,35 @@ pub fn set_window(app: &mut App, config: &PakeConfig, tauri_config: &Config) -> .initialization_script(include_str!("../inject/style.js")) .initialization_script(include_str!("../inject/custom.js")); + #[cfg(target_os = "windows")] + let mut windows_browser_args = String::from("--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --disable-blink-features=AutomationControlled"); + + #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))] + let mut linux_browser_args = String::from("--disable-blink-features=AutomationControlled"); + if window_config.enable_wasm { - window_builder = window_builder - .additional_browser_args("--enable-features=SharedArrayBuffer") - .additional_browser_args("--enable-unsafe-webgpu"); + #[cfg(target_os = "windows")] + { + windows_browser_args.push_str(" --enable-features=SharedArrayBuffer"); + windows_browser_args.push_str(" --enable-unsafe-webgpu"); + } + + #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))] + { + linux_browser_args.push_str(" --enable-features=SharedArrayBuffer"); + linux_browser_args.push_str(" --enable-unsafe-webgpu"); + } + + #[cfg(target_os = "macos")] + { + window_builder = window_builder + .additional_browser_args("--enable-features=SharedArrayBuffer") + .additional_browser_args("--enable-unsafe-webgpu"); + } } + let mut parsed_proxy_url: Option = None; + // Platform-specific configuration must be set before proxy on Windows/Linux #[cfg(target_os = "macos")] { @@ -84,20 +123,44 @@ pub fn set_window(app: &mut App, config: &PakeConfig, tauri_config: &Config) -> // Windows and Linux: set data_directory before proxy_url #[cfg(not(target_os = "macos"))] { - window_builder = window_builder - .data_directory(_data_dir) - .additional_browser_args("--disable-blink-features=AutomationControlled") - .theme(None); + window_builder = window_builder.data_directory(_data_dir).theme(None); + + if !config.proxy_url.is_empty() { + if let Ok(proxy_url) = Url::from_str(&config.proxy_url) { + parsed_proxy_url = Some(proxy_url.clone()); + #[cfg(target_os = "windows")] + { + if let Some(arg) = build_proxy_browser_arg(&proxy_url) { + windows_browser_args.push(' '); + windows_browser_args.push_str(&arg); + } + } + } + } + + #[cfg(target_os = "windows")] + { + window_builder = window_builder.additional_browser_args(&windows_browser_args); + } + + #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))] + { + window_builder = window_builder.additional_browser_args(&linux_browser_args); + } } // Set proxy after platform-specific configs (required for Windows/Linux) - if !config.proxy_url.is_empty() { + if parsed_proxy_url.is_none() && !config.proxy_url.is_empty() { if let Ok(proxy_url) = Url::from_str(&config.proxy_url) { - window_builder = window_builder.proxy_url(proxy_url); - #[cfg(debug_assertions)] - println!("Proxy configured: {}", config.proxy_url); + parsed_proxy_url = Some(proxy_url); } } + if let Some(proxy_url) = parsed_proxy_url { + window_builder = window_builder.proxy_url(proxy_url); + #[cfg(debug_assertions)] + println!("Proxy configured: {}", config.proxy_url); + } + window_builder.build().expect("Failed to build window") }