pake-cli 3.11.4 โ 3.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli.js +330 -161
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/gen/schemas/acl-manifests.json +1 -0
- package/src-tauri/gen/schemas/capabilities.json +1 -0
- package/src-tauri/gen/schemas/desktop-schema.json +3331 -0
- package/src-tauri/gen/schemas/macOS-schema.json +3331 -0
- package/src-tauri/src/app/setup.rs +39 -34
- package/src-tauri/src/app/window.rs +63 -17
- package/src-tauri/src/inject/{component.js โ fullscreen.js} +7 -41
- package/src-tauri/src/inject/toast.js +22 -0
- package/src-tauri/src/lib.rs +5 -2
- package/src-tauri/src/util.rs +131 -24
- package/src-tauri/tauri.conf.json +1 -1
package/README.md
CHANGED
|
@@ -177,7 +177,7 @@ First-time packaging requires environment setup and may be slower, subsequent bu
|
|
|
177
177
|
|
|
178
178
|
## Development
|
|
179
179
|
|
|
180
|
-
Requires Rust `>=1.85` and Node `>=22
|
|
180
|
+
Requires Rust `>=1.85` and Node `>=22` (recommended LTS; `>=18` also works). For detailed installation guide, see [Tauri documentation](https://v2.tauri.app/start/prerequisites/). If unfamiliar with development environment, use the CLI tool instead.
|
|
181
181
|
|
|
182
182
|
```bash
|
|
183
183
|
# Install dependencies
|
|
@@ -204,6 +204,6 @@ Pake's development can not be without these Hackers. They contributed a lot of c
|
|
|
204
204
|
|
|
205
205
|
- If Pake helped you, [share it](https://twitter.com/intent/tweet?url=https://github.com/tw93/Pake&text=Pake%20-%20Turn%20any%20webpage%20into%20a%20desktop%20app%20with%20one%20command.%20Nearly%2020x%20smaller%20than%20Electron%20packages,%20supports%20macOS%20Windows%20Linux) with friends or give it a star.
|
|
206
206
|
- Got ideas or bugs? Open an issue or PR, feel free to contribute your best AI model.
|
|
207
|
-
- I have two cats, TangYuan and Coke. If you think Pake delights your life, you can feed them <a href="https://
|
|
207
|
+
- I have two cats, TangYuan and Coke. If you think Pake delights your life, you can feed them <a href="https://cats.tw93.fun?name=Pake" target="_blank">canned food ๐ฅฉ</a>.
|
|
208
208
|
|
|
209
|
-
<a href="https://
|
|
209
|
+
<a href="https://cats.tw93.fun?name=Pake"><img src="https://cdn.jsdelivr.net/gh/tw93/sponsors@main/assets/sponsors.svg" width="1000" loading="lazy" /></a>
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import log from 'loglevel';
|
|
3
|
+
import chalk from 'chalk';
|
|
3
4
|
import updateNotifier from 'update-notifier';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import fsExtra from 'fs-extra';
|
|
6
7
|
import { fileURLToPath } from 'url';
|
|
7
|
-
import chalk from 'chalk';
|
|
8
8
|
import prompts from 'prompts';
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import { execa, execaSync } from 'execa';
|
|
@@ -20,7 +20,7 @@ import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
|
20
20
|
import fs$1 from 'fs';
|
|
21
21
|
|
|
22
22
|
var name = "pake-cli";
|
|
23
|
-
var version = "3.11.
|
|
23
|
+
var version = "3.11.5";
|
|
24
24
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
25
25
|
var engines = {
|
|
26
26
|
node: ">=18.0.0"
|
|
@@ -436,6 +436,41 @@ function generateIdentifierSafeName(name) {
|
|
|
436
436
|
return cleaned;
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Pure transform from CLI options to the window-config slice that gets
|
|
441
|
+
* merged into pake.json. Exposed for snapshot testing so option drift
|
|
442
|
+
* (e.g. a new flag added in cli-program.ts but forgotten here) is caught.
|
|
443
|
+
*
|
|
444
|
+
* Keep this function side-effect free.
|
|
445
|
+
*/
|
|
446
|
+
function buildWindowConfigOverrides(options, platform = asSupportedPlatform(process.platform)) {
|
|
447
|
+
const platformHideOnClose = options.hideOnClose ?? platform === 'darwin';
|
|
448
|
+
return {
|
|
449
|
+
width: options.width,
|
|
450
|
+
height: options.height,
|
|
451
|
+
fullscreen: options.fullscreen,
|
|
452
|
+
maximize: options.maximize,
|
|
453
|
+
resizable: options.resizable ?? true,
|
|
454
|
+
hide_title_bar: options.hideTitleBar,
|
|
455
|
+
activation_shortcut: options.activationShortcut,
|
|
456
|
+
always_on_top: options.alwaysOnTop,
|
|
457
|
+
dark_mode: options.darkMode,
|
|
458
|
+
disabled_web_shortcuts: options.disabledWebShortcuts,
|
|
459
|
+
hide_on_close: platformHideOnClose,
|
|
460
|
+
incognito: options.incognito,
|
|
461
|
+
title: options.title,
|
|
462
|
+
enable_wasm: options.wasm,
|
|
463
|
+
enable_drag_drop: options.enableDragDrop,
|
|
464
|
+
start_to_tray: options.startToTray && options.showSystemTray,
|
|
465
|
+
force_internal_navigation: options.forceInternalNavigation,
|
|
466
|
+
internal_url_regex: options.internalUrlRegex,
|
|
467
|
+
zoom: options.zoom,
|
|
468
|
+
min_width: options.minWidth,
|
|
469
|
+
min_height: options.minHeight,
|
|
470
|
+
ignore_certificate_errors: options.ignoreCertificateErrors,
|
|
471
|
+
new_window: options.newWindow,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
439
474
|
function asSupportedPlatform(platform) {
|
|
440
475
|
if (platform !== 'win32' && platform !== 'darwin' && platform !== 'linux') {
|
|
441
476
|
throw new Error(`Pake only supports win32, darwin, and linux; detected '${platform}'.`);
|
|
@@ -691,34 +726,9 @@ async function writeAllConfigs(tauriConf, platform) {
|
|
|
691
726
|
}
|
|
692
727
|
async function mergeConfig(url, options, tauriConf) {
|
|
693
728
|
await copyTemplateConfigs();
|
|
694
|
-
const {
|
|
729
|
+
const { appVersion, userAgent, showSystemTray, useLocalFile, identifier, name = 'pake-app', installerLanguage, wasm, camera, microphone, } = options;
|
|
695
730
|
const platform = asSupportedPlatform(process.platform);
|
|
696
|
-
const
|
|
697
|
-
const tauriConfWindowOptions = {
|
|
698
|
-
width,
|
|
699
|
-
height,
|
|
700
|
-
fullscreen,
|
|
701
|
-
maximize,
|
|
702
|
-
resizable,
|
|
703
|
-
hide_title_bar: hideTitleBar,
|
|
704
|
-
activation_shortcut: activationShortcut,
|
|
705
|
-
always_on_top: alwaysOnTop,
|
|
706
|
-
dark_mode: darkMode,
|
|
707
|
-
disabled_web_shortcuts: disabledWebShortcuts,
|
|
708
|
-
hide_on_close: platformHideOnClose,
|
|
709
|
-
incognito,
|
|
710
|
-
title,
|
|
711
|
-
enable_wasm: wasm,
|
|
712
|
-
enable_drag_drop: enableDragDrop,
|
|
713
|
-
start_to_tray: startToTray && showSystemTray,
|
|
714
|
-
force_internal_navigation: forceInternalNavigation,
|
|
715
|
-
internal_url_regex: internalUrlRegex,
|
|
716
|
-
zoom,
|
|
717
|
-
min_width: minWidth,
|
|
718
|
-
min_height: minHeight,
|
|
719
|
-
ignore_certificate_errors: ignoreCertificateErrors,
|
|
720
|
-
new_window: newWindow,
|
|
721
|
-
};
|
|
731
|
+
const tauriConfWindowOptions = buildWindowConfigOverrides(options, platform);
|
|
722
732
|
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
|
|
723
733
|
tauriConf.productName = name;
|
|
724
734
|
tauriConf.identifier = identifier;
|
|
@@ -764,104 +774,138 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
764
774
|
await writeAllConfigs(tauriConf, platform);
|
|
765
775
|
}
|
|
766
776
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const currentPath = process.env.PATH || '';
|
|
776
|
-
const systemToolsPath = '/usr/bin';
|
|
777
|
-
const buildPath = currentPath.startsWith(`${systemToolsPath}:`)
|
|
778
|
-
? currentPath
|
|
779
|
-
: `${systemToolsPath}:${currentPath}`;
|
|
780
|
-
return {
|
|
781
|
-
CFLAGS: '-fno-modules',
|
|
782
|
-
CXXFLAGS: '-fno-modules',
|
|
783
|
-
MACOSX_DEPLOYMENT_TARGET: '14.0',
|
|
784
|
-
PATH: buildPath,
|
|
785
|
-
};
|
|
777
|
+
/**
|
|
778
|
+
* Returns build environment variables overrides for macOS, where Rust crates
|
|
779
|
+
* sometimes need explicit C/C++ flags and a deterministic SDK target. Other
|
|
780
|
+
* platforms inherit `process.env` unchanged.
|
|
781
|
+
*/
|
|
782
|
+
function getBuildEnvironment() {
|
|
783
|
+
if (!IS_MAC) {
|
|
784
|
+
return undefined;
|
|
786
785
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
786
|
+
const currentPath = process.env.PATH || '';
|
|
787
|
+
const systemToolsPath = '/usr/bin';
|
|
788
|
+
const buildPath = currentPath.startsWith(`${systemToolsPath}:`)
|
|
789
|
+
? currentPath
|
|
790
|
+
: `${systemToolsPath}:${currentPath}`;
|
|
791
|
+
return {
|
|
792
|
+
CFLAGS: '-fno-modules',
|
|
793
|
+
CXXFLAGS: '-fno-modules',
|
|
794
|
+
MACOSX_DEPLOYMENT_TARGET: '14.0',
|
|
795
|
+
PATH: buildPath,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Windows needs more time due to native compilation and antivirus scanning.
|
|
800
|
+
*/
|
|
801
|
+
function getInstallTimeout() {
|
|
802
|
+
return process.platform === 'win32' ? 900000 : 600000;
|
|
803
|
+
}
|
|
804
|
+
function getBuildTimeout() {
|
|
805
|
+
return 900000;
|
|
806
|
+
}
|
|
807
|
+
let packageManagerCache = null;
|
|
808
|
+
/**
|
|
809
|
+
* Returns 'pnpm' when available, otherwise 'npm'. Throws if neither is found.
|
|
810
|
+
* Cached after the first successful detection so tests can call repeatedly.
|
|
811
|
+
*/
|
|
812
|
+
async function detectPackageManager() {
|
|
813
|
+
if (packageManagerCache) {
|
|
814
|
+
return packageManagerCache;
|
|
790
815
|
}
|
|
791
|
-
|
|
792
|
-
|
|
816
|
+
const { execa } = await import('execa');
|
|
817
|
+
try {
|
|
818
|
+
await execa('pnpm', ['--version'], { stdio: 'ignore' });
|
|
819
|
+
logger.info('โบ Using pnpm for package management.');
|
|
820
|
+
packageManagerCache = 'pnpm';
|
|
821
|
+
return 'pnpm';
|
|
793
822
|
}
|
|
794
|
-
|
|
795
|
-
if (BaseBuilder.packageManagerCache) {
|
|
796
|
-
return BaseBuilder.packageManagerCache;
|
|
797
|
-
}
|
|
798
|
-
const { execa } = await import('execa');
|
|
823
|
+
catch {
|
|
799
824
|
try {
|
|
800
|
-
await execa('
|
|
801
|
-
logger.info('โบ
|
|
802
|
-
|
|
803
|
-
return '
|
|
825
|
+
await execa('npm', ['--version'], { stdio: 'ignore' });
|
|
826
|
+
logger.info('โบ pnpm not available, using npm for package management.');
|
|
827
|
+
packageManagerCache = 'npm';
|
|
828
|
+
return 'npm';
|
|
804
829
|
}
|
|
805
830
|
catch {
|
|
806
|
-
|
|
807
|
-
await execa('npm', ['--version'], { stdio: 'ignore' });
|
|
808
|
-
logger.info('โบ pnpm not available, using npm for package management.');
|
|
809
|
-
BaseBuilder.packageManagerCache = 'npm';
|
|
810
|
-
return 'npm';
|
|
811
|
-
}
|
|
812
|
-
catch {
|
|
813
|
-
throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
|
|
814
|
-
}
|
|
831
|
+
throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
|
|
815
832
|
}
|
|
816
833
|
}
|
|
817
|
-
|
|
818
|
-
|
|
834
|
+
}
|
|
835
|
+
function getInstallCommand(packageManager, useCnMirror) {
|
|
836
|
+
const registryOption = useCnMirror
|
|
837
|
+
? ' --registry=https://registry.npmmirror.com'
|
|
838
|
+
: '';
|
|
839
|
+
const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
|
|
840
|
+
return `cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`;
|
|
841
|
+
}
|
|
842
|
+
async function copyFileWithSamePathGuard(sourcePath, destinationPath) {
|
|
843
|
+
if (path.resolve(sourcePath) === path.resolve(destinationPath)) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
await fsExtra.copy(sourcePath, destinationPath, { overwrite: true });
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
if (error instanceof Error &&
|
|
851
|
+
error.message.includes('Source and destination must not be the same')) {
|
|
819
852
|
return;
|
|
820
853
|
}
|
|
821
|
-
|
|
822
|
-
await fsExtra.copy(sourcePath, destinationPath, { overwrite: true });
|
|
823
|
-
}
|
|
824
|
-
catch (error) {
|
|
825
|
-
if (error instanceof Error &&
|
|
826
|
-
error.message.includes('Source and destination must not be the same')) {
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
throw error;
|
|
830
|
-
}
|
|
854
|
+
throw error;
|
|
831
855
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
856
|
+
}
|
|
857
|
+
function isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig) {
|
|
858
|
+
return projectConfig.trim() === cnMirrorConfig.trim();
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Toggles `.cargo/config.toml` to point at rsproxy.cn when the user opts in
|
|
862
|
+
* via `PAKE_USE_CN_MIRROR=1`, and removes the auto-generated mirror config
|
|
863
|
+
* (or warns about a manual one) when they opt out.
|
|
864
|
+
*/
|
|
865
|
+
async function configureCargoRegistry(tauriSrcPath, useCnMirror) {
|
|
866
|
+
const rustProjectDir = path.join(tauriSrcPath, '.cargo');
|
|
867
|
+
const projectConf = path.join(rustProjectDir, 'config.toml');
|
|
868
|
+
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
|
|
869
|
+
if (useCnMirror) {
|
|
870
|
+
await fsExtra.ensureDir(rustProjectDir);
|
|
871
|
+
await copyFileWithSamePathGuard(projectCnConf, projectConf);
|
|
872
|
+
return;
|
|
838
873
|
}
|
|
839
|
-
|
|
840
|
-
return
|
|
874
|
+
if (!(await fsExtra.pathExists(projectConf))) {
|
|
875
|
+
return;
|
|
841
876
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
877
|
+
const [projectConfig, cnMirrorConfig] = await Promise.all([
|
|
878
|
+
fsExtra.readFile(projectConf, 'utf8'),
|
|
879
|
+
fsExtra.readFile(projectCnConf, 'utf8'),
|
|
880
|
+
]);
|
|
881
|
+
if (isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig)) {
|
|
882
|
+
await fsExtra.remove(projectConf);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (projectConfig.includes('rsproxy.cn')) {
|
|
886
|
+
logger.warn(`โผ ${projectConf} still references rsproxy.cn. Remove it or set ${CN_MIRROR_ENV}=1 if you want to use the CN mirror.`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Returns true when an error string looks like the well-known Tauri+linuxdeploy
|
|
891
|
+
* strip failure that we automatically retry with NO_STRIP=1.
|
|
892
|
+
*/
|
|
893
|
+
function isLinuxDeployStripError(error) {
|
|
894
|
+
if (!(error instanceof Error) || !error.message) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
const message = error.message.toLowerCase();
|
|
898
|
+
return (message.includes('linuxdeploy') ||
|
|
899
|
+
message.includes('failed to run linuxdeploy') ||
|
|
900
|
+
message.includes('strip:') ||
|
|
901
|
+
message.includes('unable to recognise the format of the input file') ||
|
|
902
|
+
message.includes('appimage tool failed') ||
|
|
903
|
+
message.includes('strip tool'));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
class BaseBuilder {
|
|
907
|
+
constructor(options) {
|
|
908
|
+
this.options = options;
|
|
865
909
|
}
|
|
866
910
|
async prepare() {
|
|
867
911
|
const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
|
|
@@ -888,11 +932,10 @@ class BaseBuilder {
|
|
|
888
932
|
}
|
|
889
933
|
const spinner = getSpinner('Installing package...');
|
|
890
934
|
const useCnMirror = isCnMirrorEnabled();
|
|
891
|
-
await
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
const
|
|
895
|
-
const buildEnv = this.getBuildEnvironment();
|
|
935
|
+
await configureCargoRegistry(tauriSrcPath, useCnMirror);
|
|
936
|
+
const packageManager = await detectPackageManager();
|
|
937
|
+
const timeout = getInstallTimeout();
|
|
938
|
+
const buildEnv = getBuildEnvironment();
|
|
896
939
|
// Show helpful message for first-time users
|
|
897
940
|
if (!tauriTargetPathExists) {
|
|
898
941
|
logger.info(process.platform === 'win32'
|
|
@@ -903,7 +946,7 @@ class BaseBuilder {
|
|
|
903
946
|
logger.info(`โบ ${CN_MIRROR_ENV}=1 detected, using ${packageManager}/rsProxy CN mirror.`);
|
|
904
947
|
}
|
|
905
948
|
try {
|
|
906
|
-
await shellExec(
|
|
949
|
+
await shellExec(getInstallCommand(packageManager, useCnMirror), timeout, {
|
|
907
950
|
...buildEnv,
|
|
908
951
|
CI: 'true',
|
|
909
952
|
});
|
|
@@ -926,7 +969,7 @@ class BaseBuilder {
|
|
|
926
969
|
async start(url) {
|
|
927
970
|
logger.info('Pake dev server starting...');
|
|
928
971
|
await mergeConfig(url, this.options, tauriConfig);
|
|
929
|
-
const packageManager = await
|
|
972
|
+
const packageManager = await detectPackageManager();
|
|
930
973
|
const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
|
|
931
974
|
const features = this.getBuildFeatures();
|
|
932
975
|
const featureArgs = features.length > 0 ? `--features ${features.join(',')}` : '';
|
|
@@ -937,8 +980,7 @@ class BaseBuilder {
|
|
|
937
980
|
async buildAndCopy(url, target) {
|
|
938
981
|
const { name = 'pake-app' } = this.options;
|
|
939
982
|
await mergeConfig(url, this.options, tauriConfig);
|
|
940
|
-
|
|
941
|
-
const packageManager = await this.detectPackageManager();
|
|
983
|
+
const packageManager = await detectPackageManager();
|
|
942
984
|
// Build app
|
|
943
985
|
const buildSpinner = getSpinner('Building app...');
|
|
944
986
|
// Let spinner run for a moment so user can see it, then stop before package manager command
|
|
@@ -946,7 +988,7 @@ class BaseBuilder {
|
|
|
946
988
|
buildSpinner.stop();
|
|
947
989
|
// Show static message to keep the status visible
|
|
948
990
|
logger.warn('โธ Building app...');
|
|
949
|
-
const baseEnv =
|
|
991
|
+
const baseEnv = getBuildEnvironment();
|
|
950
992
|
let buildEnv = {
|
|
951
993
|
...(baseEnv ?? {}),
|
|
952
994
|
...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
|
|
@@ -962,7 +1004,7 @@ class BaseBuilder {
|
|
|
962
1004
|
}
|
|
963
1005
|
}
|
|
964
1006
|
const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
|
|
965
|
-
const buildTimeout =
|
|
1007
|
+
const buildTimeout = getBuildTimeout();
|
|
966
1008
|
try {
|
|
967
1009
|
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
|
|
968
1010
|
}
|
|
@@ -970,7 +1012,7 @@ class BaseBuilder {
|
|
|
970
1012
|
const shouldRetryWithoutStrip = process.platform === 'linux' &&
|
|
971
1013
|
target === 'appimage' &&
|
|
972
1014
|
!buildEnv.NO_STRIP &&
|
|
973
|
-
|
|
1015
|
+
isLinuxDeployStripError(error);
|
|
974
1016
|
if (shouldRetryWithoutStrip) {
|
|
975
1017
|
logger.warn('โ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.');
|
|
976
1018
|
buildEnv = {
|
|
@@ -1026,18 +1068,6 @@ class BaseBuilder {
|
|
|
1026
1068
|
getFileType(target) {
|
|
1027
1069
|
return target;
|
|
1028
1070
|
}
|
|
1029
|
-
isLinuxDeployStripError(error) {
|
|
1030
|
-
if (!(error instanceof Error) || !error.message) {
|
|
1031
|
-
return false;
|
|
1032
|
-
}
|
|
1033
|
-
const message = error.message.toLowerCase();
|
|
1034
|
-
return (message.includes('linuxdeploy') ||
|
|
1035
|
-
message.includes('failed to run linuxdeploy') ||
|
|
1036
|
-
message.includes('strip:') ||
|
|
1037
|
-
message.includes('unable to recognise the format of the input file') ||
|
|
1038
|
-
message.includes('appimage tool failed') ||
|
|
1039
|
-
message.includes('strip tool'));
|
|
1040
|
-
}
|
|
1041
1071
|
resolveTargetArch(requestedArch) {
|
|
1042
1072
|
if (requestedArch === 'auto' || !requestedArch) {
|
|
1043
1073
|
return process.arch;
|
|
@@ -1175,7 +1205,6 @@ class BaseBuilder {
|
|
|
1175
1205
|
return 'src-tauri/target'; // Override in subclasses if needed
|
|
1176
1206
|
}
|
|
1177
1207
|
}
|
|
1178
|
-
BaseBuilder.packageManagerCache = null;
|
|
1179
1208
|
BaseBuilder.ARCH_MAPPINGS = {
|
|
1180
1209
|
darwin: {
|
|
1181
1210
|
arm64: 'aarch64-apple-darwin',
|
|
@@ -1511,6 +1540,9 @@ function getIconSourcePriority(url, appName) {
|
|
|
1511
1540
|
const ICO_HEADER_SIZE = 6;
|
|
1512
1541
|
const ICO_DIR_ENTRY_SIZE = 16;
|
|
1513
1542
|
const ICO_TYPE_ICON = 1;
|
|
1543
|
+
// Standard Windows icon sizes covering tray (16/24/32), taskbar (32/48),
|
|
1544
|
+
// shell (48/256) and high-DPI (128/256). Issue #1190.
|
|
1545
|
+
const WIN_STANDARD_ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];
|
|
1514
1546
|
function decodeDimension(value) {
|
|
1515
1547
|
return value === 0 ? 256 : value;
|
|
1516
1548
|
}
|
|
@@ -1609,6 +1641,91 @@ async function writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize)
|
|
|
1609
1641
|
return false;
|
|
1610
1642
|
}
|
|
1611
1643
|
}
|
|
1644
|
+
/**
|
|
1645
|
+
* PNG signature `\x89PNG`. ICO frames may carry either a BMP DIB or an
|
|
1646
|
+
* embedded PNG payload (PNG-in-ICO, supported since Windows Vista).
|
|
1647
|
+
*/
|
|
1648
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
|
|
1649
|
+
function frameLooksLikePng(entry) {
|
|
1650
|
+
return (entry.data.length >= PNG_SIGNATURE.length &&
|
|
1651
|
+
entry.data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE));
|
|
1652
|
+
}
|
|
1653
|
+
async function decodeFrameToPng(entry) {
|
|
1654
|
+
if (frameLooksLikePng(entry)) {
|
|
1655
|
+
return Buffer.from(entry.data);
|
|
1656
|
+
}
|
|
1657
|
+
// BMP DIB frames need to go through sharp's ico-to-PNG path, which only
|
|
1658
|
+
// works on the full ICO container. Fall back to letting the caller use a
|
|
1659
|
+
// sharp pipeline against the original ICO for the missing source.
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
async function pickLargestFrameAsPng(buffer, entries) {
|
|
1663
|
+
const largest = [...entries].sort((a, b) => Math.max(b.width, b.height) - Math.max(a.width, a.height))[0];
|
|
1664
|
+
if (largest) {
|
|
1665
|
+
const decoded = await decodeFrameToPng(largest);
|
|
1666
|
+
if (decoded) {
|
|
1667
|
+
return decoded;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
// Fallback: let sharp render directly from the ICO buffer. sharp picks the
|
|
1671
|
+
// largest embedded frame on its own.
|
|
1672
|
+
try {
|
|
1673
|
+
return await sharp(buffer).png().toBuffer();
|
|
1674
|
+
}
|
|
1675
|
+
catch {
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Ensures the produced ICO carries every Windows standard size so the OS
|
|
1681
|
+
* never has to downsample a 256x256 frame to 16x16 for the tray.
|
|
1682
|
+
* Falls back to `writeIcoWithPreferredSize` if rendering fails.
|
|
1683
|
+
*
|
|
1684
|
+
* Issue #1190.
|
|
1685
|
+
*/
|
|
1686
|
+
async function ensureMultiResolutionIco(sourcePath, outputPath, preferredSize = 256, desiredSizes = WIN_STANDARD_ICO_SIZES) {
|
|
1687
|
+
try {
|
|
1688
|
+
const sourceBuffer = await fsExtra.readFile(sourcePath);
|
|
1689
|
+
const entries = parseIcoBuffer(sourceBuffer);
|
|
1690
|
+
const sourcePng = await pickLargestFrameAsPng(sourceBuffer, entries);
|
|
1691
|
+
if (!sourcePng) {
|
|
1692
|
+
return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
|
|
1693
|
+
}
|
|
1694
|
+
const frames = await Promise.all(desiredSizes.map(async (size) => {
|
|
1695
|
+
// Reuse an existing exact-size PNG frame when possible to keep any
|
|
1696
|
+
// hand-tuned small icon (e.g. a 16x16 with deliberate pixel hinting).
|
|
1697
|
+
const exact = entries.find((entry) => entry.width === size && entry.height === size);
|
|
1698
|
+
if (exact && frameLooksLikePng(exact)) {
|
|
1699
|
+
return { size, png: Buffer.from(exact.data) };
|
|
1700
|
+
}
|
|
1701
|
+
const png = await sharp(sourcePng)
|
|
1702
|
+
.resize(size, size, {
|
|
1703
|
+
fit: 'contain',
|
|
1704
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
1705
|
+
})
|
|
1706
|
+
.ensureAlpha()
|
|
1707
|
+
.png()
|
|
1708
|
+
.toBuffer();
|
|
1709
|
+
return { size, png };
|
|
1710
|
+
}));
|
|
1711
|
+
// Order frames so the preferred size lands first (Windows shell uses the
|
|
1712
|
+
// first-listed frame as a quality hint when choosing which to display).
|
|
1713
|
+
frames.sort((a, b) => {
|
|
1714
|
+
const aExact = a.size === preferredSize ? 0 : 1;
|
|
1715
|
+
const bExact = b.size === preferredSize ? 0 : 1;
|
|
1716
|
+
if (aExact !== bExact)
|
|
1717
|
+
return aExact - bExact;
|
|
1718
|
+
return b.size - a.size;
|
|
1719
|
+
});
|
|
1720
|
+
const icoBuffer = buildIcoFromPngBuffers(frames);
|
|
1721
|
+
await fsExtra.ensureDir(path.dirname(outputPath));
|
|
1722
|
+
await fsExtra.outputFile(outputPath, icoBuffer);
|
|
1723
|
+
return true;
|
|
1724
|
+
}
|
|
1725
|
+
catch {
|
|
1726
|
+
return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1612
1729
|
/**
|
|
1613
1730
|
* Builds an ICO file from an array of PNG buffers using the PNG-in-ICO format
|
|
1614
1731
|
* (supported since Windows Vista). This preserves alpha transparency.
|
|
@@ -1658,7 +1775,7 @@ const ICON_CONFIG = {
|
|
|
1658
1775
|
},
|
|
1659
1776
|
};
|
|
1660
1777
|
const PLATFORM_CONFIG = {
|
|
1661
|
-
win: { format: '.ico', sizes: [
|
|
1778
|
+
win: { format: '.ico', sizes: [...WIN_STANDARD_ICO_SIZES] },
|
|
1662
1779
|
linux: { format: '.png', size: 512 },
|
|
1663
1780
|
macos: { format: '.icns', sizes: [16, 32, 64, 128, 256, 512, 1024] },
|
|
1664
1781
|
};
|
|
@@ -1693,10 +1810,15 @@ async function copyWindowsIconIfNeeded(convertedPath, appName) {
|
|
|
1693
1810
|
try {
|
|
1694
1811
|
const finalIconPath = generateIconPath(appName);
|
|
1695
1812
|
await fsExtra.ensureDir(path.dirname(finalIconPath));
|
|
1696
|
-
//
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1813
|
+
// Re-render ICO so every Windows standard size is present and prefer the
|
|
1814
|
+
// 256px frame as the leading entry; falls back to plain reordering if the
|
|
1815
|
+
// ICO is non-decodable, then to a raw copy. (Issue #1190)
|
|
1816
|
+
const upgraded = await ensureMultiResolutionIco(convertedPath, finalIconPath, 256);
|
|
1817
|
+
if (!upgraded) {
|
|
1818
|
+
const reordered = await writeIcoWithPreferredSize(convertedPath, finalIconPath, 256);
|
|
1819
|
+
if (!reordered) {
|
|
1820
|
+
await fsExtra.copy(convertedPath, finalIconPath);
|
|
1821
|
+
}
|
|
1700
1822
|
}
|
|
1701
1823
|
return finalIconPath;
|
|
1702
1824
|
}
|
|
@@ -2153,6 +2275,28 @@ function normalizeUrl(urlToNormalize) {
|
|
|
2153
2275
|
}
|
|
2154
2276
|
}
|
|
2155
2277
|
|
|
2278
|
+
/**
|
|
2279
|
+
* Error class used for user-facing CLI errors.
|
|
2280
|
+
*
|
|
2281
|
+
* The top-level catch in `bin/cli.ts` prints `message` directly without a
|
|
2282
|
+
* stack trace and exits with code 1. Use this for predictable failures
|
|
2283
|
+
* (invalid names, missing files, etc.) so users see a clean message instead
|
|
2284
|
+
* of a Node.js stack dump.
|
|
2285
|
+
*/
|
|
2286
|
+
class PakeError extends Error {
|
|
2287
|
+
constructor(message) {
|
|
2288
|
+
super(message);
|
|
2289
|
+
this.isUserError = true;
|
|
2290
|
+
this.name = 'PakeError';
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
function isPakeError(error) {
|
|
2294
|
+
return (error instanceof PakeError ||
|
|
2295
|
+
(typeof error === 'object' &&
|
|
2296
|
+
error !== null &&
|
|
2297
|
+
error.isUserError === true));
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2156
2300
|
function resolveAppName(name, platform) {
|
|
2157
2301
|
const domain = getDomain(name) || 'pake';
|
|
2158
2302
|
return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
|
|
@@ -2195,13 +2339,13 @@ async function handleOptions(options, url) {
|
|
|
2195
2339
|
const LINUX_NAME_ERROR = `โ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;
|
|
2196
2340
|
const DEFAULT_NAME_ERROR = `โ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, 123.`;
|
|
2197
2341
|
const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
|
|
2198
|
-
logger.error(errorMsg);
|
|
2199
2342
|
if (isActions) {
|
|
2343
|
+
logger.error(errorMsg);
|
|
2200
2344
|
name = resolveAppName(url, platform);
|
|
2201
2345
|
logger.warn(`โผ Inside github actions, use the default name: ${name}`);
|
|
2202
2346
|
}
|
|
2203
2347
|
else {
|
|
2204
|
-
|
|
2348
|
+
throw new PakeError(errorMsg);
|
|
2205
2349
|
}
|
|
2206
2350
|
}
|
|
2207
2351
|
const resolvedName = name || 'pake-app';
|
|
@@ -2459,21 +2603,46 @@ async function checkUpdateTips() {
|
|
|
2459
2603
|
});
|
|
2460
2604
|
}
|
|
2461
2605
|
program.action(async (url, options) => {
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2606
|
+
try {
|
|
2607
|
+
await checkUpdateTips();
|
|
2608
|
+
if (!url) {
|
|
2609
|
+
program.help({
|
|
2610
|
+
error: false,
|
|
2611
|
+
});
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
log.setDefaultLevel('info');
|
|
2615
|
+
log.setLevel('info');
|
|
2616
|
+
if (options.debug) {
|
|
2617
|
+
log.setLevel('debug');
|
|
2618
|
+
}
|
|
2619
|
+
const appOptions = await handleOptions(options, url);
|
|
2620
|
+
const builder = BuilderProvider.create(appOptions);
|
|
2621
|
+
await builder.prepare();
|
|
2622
|
+
await builder.build(url);
|
|
2623
|
+
}
|
|
2624
|
+
catch (error) {
|
|
2625
|
+
if (isPakeError(error)) {
|
|
2626
|
+
console.error(chalk.red(error.message));
|
|
2627
|
+
}
|
|
2628
|
+
else if (error instanceof Error) {
|
|
2629
|
+
console.error(chalk.red(`โ ${error.message}`));
|
|
2630
|
+
if (options?.debug && error.stack) {
|
|
2631
|
+
console.error(chalk.gray(error.stack));
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
else {
|
|
2635
|
+
console.error(chalk.red(`โ Unexpected error: ${String(error)}`));
|
|
2636
|
+
}
|
|
2637
|
+
process.exit(1);
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2640
|
+
program.parseAsync().catch((error) => {
|
|
2641
|
+
if (error instanceof Error) {
|
|
2642
|
+
console.error(chalk.red(`โ ${error.message}`));
|
|
2468
2643
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
if (options.debug) {
|
|
2472
|
-
log.setLevel('debug');
|
|
2644
|
+
else {
|
|
2645
|
+
console.error(chalk.red(`โ Unexpected error: ${String(error)}`));
|
|
2473
2646
|
}
|
|
2474
|
-
|
|
2475
|
-
const builder = BuilderProvider.create(appOptions);
|
|
2476
|
-
await builder.prepare();
|
|
2477
|
-
await builder.build(url);
|
|
2647
|
+
process.exit(1);
|
|
2478
2648
|
});
|
|
2479
|
-
program.parse();
|