Skip to content

Commit 21e13f8

Browse files
committed
Intermediate commit for bundled cli fixes
1 parent bc4c25b commit 21e13f8

23 files changed

Lines changed: 632 additions & 75 deletions

File tree

electrobun.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ export default {
4242
bundleCEF: false,
4343
icons: "icons/icon.iconset",
4444
/**
45-
* Bundled Ghostscript/ImageMagick need `DYLD_LIBRARY_PATH` (set in `bundled_cli_apply_embedded_runtime_env`)
46-
* so `gs`/`magick` resolve `libjbig2dec` and other merged keg dylibs. Hardened Runtime ignores that var
47-
* without this entitlement, which surfaces as `Symbol not found: _jbig2_complete_page`.
45+
* Bundled Ghostscript/ImageMagick/qpdf need `DYLD_LIBRARY_PATH` and Ghostscript needs `GS_LIB`
46+
* (set in `bundled_cli_apply_embedded_runtime_env`) so CLIs resolve merged keg dylibs and `gs_init.ps`.
47+
* Hardened Runtime ignores `DYLD_LIBRARY_PATH` without this entitlement (e.g. `Symbol not found: _jbig2_complete_page`).
4848
*/
4949
entitlements: {
5050
"com.apple.security.cs.allow-dyld-environment-variables": true,

scripts/bundled-cli/pack_layout.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,42 @@ function linux_qpdf_shared_libs_present(bin_root: string): boolean {
7373
}
7474
}
7575

76+
function darwin_qpdf_dylibs_present(bin_root: string): boolean {
77+
const lib_dir = path.join(bin_root, "qpdf-lib");
78+
if (!existsSync(lib_dir)) {
79+
return false;
80+
}
81+
try {
82+
const names = readdirSync(lib_dir);
83+
return names.some((n) => n.startsWith("libqpdf.") && n.endsWith(".dylib"));
84+
} catch {
85+
return false;
86+
}
87+
}
88+
89+
/** Require Ghostscript init resources under the bundled prefix (Homebrew layout variants). */
90+
function darwin_ghostscript_gs_init_present(bin_root: string): boolean {
91+
const share_gs = path.join(bin_root, "ghostscript", "share", "ghostscript");
92+
if (!existsSync(share_gs)) {
93+
return false;
94+
}
95+
const flat_init = path.join(share_gs, "Resource", "Init", "gs_init.ps");
96+
if (existsSync(flat_init)) {
97+
return true;
98+
}
99+
try {
100+
for (const ver of readdirSync(share_gs)) {
101+
const init_file = path.join(share_gs, ver, "Resource", "Init", "gs_init.ps");
102+
if (existsSync(init_file)) {
103+
return true;
104+
}
105+
}
106+
} catch {
107+
return false;
108+
}
109+
return false;
110+
}
111+
76112
export function list_missing_staged_paths(root: string, kind?: PackPlatformKind): string[] {
77113
const k = kind ?? pack_kind_from_process();
78114
const missing: string[] = [];
@@ -84,6 +120,12 @@ export function list_missing_staged_paths(root: string, kind?: PackPlatformKind)
84120
if (k === "linux" && !linux_qpdf_shared_libs_present(root)) {
85121
missing.push("qpdf-lib/libqpdf.so*");
86122
}
123+
if (k === "darwin" && !darwin_qpdf_dylibs_present(root)) {
124+
missing.push("qpdf-lib/libqpdf*.dylib");
125+
}
126+
if (k === "darwin" && !darwin_ghostscript_gs_init_present(root)) {
127+
missing.push("ghostscript/**/Resource/Init/gs_init.ps");
128+
}
87129
return missing;
88130
}
89131

scripts/lan_release_build.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ function mac_bundled_cli_platform_key(): string {
108108
if (process.platform !== "darwin") {
109109
throw new Error("lan_release_build expects macOS for the local release build");
110110
}
111-
return process.arch === "arm64" ? "darwin-arm64" : "darwin-x64";
111+
const out = spawnSync("sysctl", ["-n", "hw.optional.arm64"], { encoding: "utf8" });
112+
if (out.status === 0 && out.stdout.trim() === "1") {
113+
return "darwin-arm64";
114+
}
115+
return "darwin-x64";
112116
}
113117

114118
/** Match lan_sync_run: non-interactive SSH bash does not load Bun from .bashrc on Ubuntu. */

scripts/stage-bundled-bins.ts

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
* `powershell.exe`. Override 7z path: `PROMETHEUS_7Z`; override PowerShell: `PROMETHEUS_PWSH`.
88
* 7-Zip is also used for Ghostscript Inno extract + ImageMagick `.7z`.
99
* Skip entirely: PROMETHEUS_SKIP_STAGE_BINS=1. Invoked from run-electrobun-with-build-at.ts before build.
10+
*
11+
* macOS: pack key follows **hardware** (Apple Silicon → `darwin-arm64`, Intel → `darwin-x64`), not `process.arch`,
12+
* so a Rosetta-translated `bun` still stages arm64 CLIs on M-series Macs.
1013
*/
1114
import { createHash } from "node:crypto";
1215
import {
@@ -118,16 +121,19 @@ function stage_script_sha256(): string {
118121
return sha256_file(script_path);
119122
}
120123

124+
function darwin_bundled_cli_platform_key(): "darwin-arm64" | "darwin-x64" {
125+
const out = spawnSync("sysctl", ["-n", "hw.optional.arm64"], { encoding: "utf8" });
126+
if (out.status === 0 && out.stdout.trim() === "1") {
127+
return "darwin-arm64";
128+
}
129+
return "darwin-x64";
130+
}
131+
121132
function host_platform_key(): string {
122-
const arch = process.arch;
123133
if (process.platform === "darwin") {
124-
if (arch === "arm64") {
125-
return "darwin-arm64";
126-
}
127-
if (arch === "x64") {
128-
return "darwin-x64";
129-
}
134+
return darwin_bundled_cli_platform_key();
130135
}
136+
const arch = process.arch;
131137
if (process.platform === "linux" && arch === "x64") {
132138
return "linux-x64";
133139
}
@@ -541,7 +547,9 @@ async function stage_yt_dlp(): Promise<void> {
541547

542548
async function stage_qpdf(): Promise<void> {
543549
if (process.platform === "darwin") {
544-
stage_macos_homebrew_bottle_binary("qpdf", "qpdf", path.join(out_dir, "qpdf"));
550+
stage_macos_homebrew_bottle_qpdf_with_libs();
551+
/** libqpdf links to Homebrew openssl (@@HOMEBREW_PREFIX@@); ship those dylibs next to qpdf. */
552+
stage_macos_merge_formula_lib_into_destinations("openssl@3", [path.join(out_dir, "qpdf-lib")]);
545553
return;
546554
}
547555
if (process.platform === "linux") {
@@ -633,17 +641,37 @@ function macos_resolve_latest_bottle_tarball(formula: string): string {
633641
return path.join(downloads_dir, bottle);
634642
}
635643

636-
function stage_macos_homebrew_bottle_binary(formula: string, binary_name: string, dest_path: string): void {
637-
const bottle_path = macos_resolve_latest_bottle_tarball(formula);
638-
const tmp = mkdtempSync(path.join(tmpdir(), `prometheus-${formula}-`));
644+
/**
645+
* qpdf binary plus libqpdf dylibs for a relocatable bundle (dyld / @rpath).
646+
*/
647+
function stage_macos_homebrew_bottle_qpdf_with_libs(): void {
648+
const bottle_path = macos_resolve_latest_bottle_tarball("qpdf");
649+
const tmp = mkdtempSync(path.join(tmpdir(), "prometheus-qpdf-prefix-"));
639650
try {
640651
run("tar", ["-xzf", bottle_path, "-C", tmp]);
641-
const found = find_file_by_basename(tmp, binary_name);
642-
if (!found) {
643-
throw new Error(`Could not find ${binary_name} in ${formula} bottle extract`);
652+
const formula_root = path.join(tmp, "qpdf");
653+
if (!existsSync(formula_root)) {
654+
throw new Error("Bottle extract missing qpdf/ (formula root)");
655+
}
656+
const versions = readdirSync(formula_root);
657+
if (versions.length < 1) {
658+
throw new Error("Empty version list for qpdf bottle");
659+
}
660+
const prefix = path.join(formula_root, versions[0]!);
661+
const bin_src = path.join(prefix, "bin", "qpdf");
662+
if (!existsSync(bin_src)) {
663+
throw new Error("qpdf bottle missing bin/qpdf");
664+
}
665+
const dest_bin = path.join(out_dir, "qpdf");
666+
copyFileSync(bin_src, dest_bin);
667+
chmodSync(dest_bin, 0o755);
668+
const lib_src = path.join(prefix, "lib");
669+
const lib_dest = path.join(out_dir, "qpdf-lib");
670+
if (!existsSync(lib_src)) {
671+
throw new Error("qpdf bottle missing lib/ (libqpdf dylibs required at runtime)");
644672
}
645-
copyFileSync(found, dest_path);
646-
chmodSync(dest_path, 0o755);
673+
rmSync(lib_dest, { recursive: true, force: true });
674+
cpSync(lib_src, lib_dest, { recursive: true, dereference: true });
647675
} finally {
648676
rmSync(tmp, { recursive: true, force: true });
649677
}

scripts/update-bundled-tool.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,19 @@ type GitHubReleaseJson = {
4646
assets?: { name?: string; browser_download_url?: string }[];
4747
};
4848

49+
function darwin_bundled_cli_platform_key(): "darwin-arm64" | "darwin-x64" {
50+
const out = spawnSync("sysctl", ["-n", "hw.optional.arm64"], { encoding: "utf8" });
51+
if (out.status === 0 && out.stdout.trim() === "1") {
52+
return "darwin-arm64";
53+
}
54+
return "darwin-x64";
55+
}
56+
4957
function host_platform_key(): string {
50-
const arch = process.arch;
5158
if (process.platform === "darwin") {
52-
if (arch === "arm64") {
53-
return "darwin-arm64";
54-
}
55-
if (arch === "x64") {
56-
return "darwin-x64";
57-
}
59+
return darwin_bundled_cli_platform_key();
5860
}
61+
const arch = process.arch;
5962
if (process.platform === "linux" && arch === "x64") {
6063
return "linux-x64";
6164
}

src/bun/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { db_init } from "../db/db";
66
import { db_logs_init_defaults } from "../db/db.logs";
77
import { db_cli_schedules_add, db_cli_schedules_list, db_cli_schedules_remove } from "../db/db.cli_schedules";
88
import { bundled_cli_apply_embedded_runtime_env } from "../utils/bundled_cli_paths";
9+
import { bundled_toolchain_cli_abort_if_invalid } from "../utils/bundled_toolchain";
910
import {
1011
CLI_EXIT_CODE_CANCELLED,
1112
CLI_EXIT_CODE_RUNTIME_ERROR,
@@ -74,6 +75,7 @@ function cli_progress_print(event: CliProgressEvent, format: CliOutputFormat, co
7475

7576
async function cli_main(): Promise<void> {
7677
bundled_cli_apply_embedded_runtime_env();
78+
bundled_toolchain_cli_abort_if_invalid();
7779
await db_init();
7880
db_logs_init_defaults();
7981

src/bun/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import { command_debug_bind_emitter } from "./tools/command-debug";
2525
import { windows_bundle_icon_apply_to_window } from "./helpers/windows_bundle_icon";
2626
import type { CommandDebugEntry } from "./types/rpc";
2727
import { bundled_cli_apply_embedded_runtime_env } from "./utils/bundled_cli_paths";
28+
import { bundled_toolchain_run_startup_validation } from "./utils/bundled_toolchain";
2829

2930
bundled_cli_apply_embedded_runtime_env();
31+
bundled_toolchain_run_startup_validation();
3032

3133
let win: BrowserWindow;
3234
const runtime_os: RuntimeOS = process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";

src/bun/rpc/requests/core.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
import { spawn } from "node:child_process";
12
import { Updater } from "electrobun/bun";
23
import { localization_t } from "../../../shared/localization";
34
import type { UpdateStatusPayload } from "../../types/rpc";
5+
import type { BundledToolchainFailure } from "../../utils/bundled_toolchain";
46
import { guard_parse } from "../../helpers/runtime_guards";
57
import { TOOL_BACKEND_PATHS, TOOL_PAGE_COPY } from "../constants";
68
import type { RpcBaseContext } from "../../types/rpc_context";
79
import { rpc_emit_update_state_changed } from "../emitters";
810
import { get_tool_page_data_params_schema } from "./core_schemas";
11+
import {
12+
bundled_toolchain_startup_failures,
13+
bundled_toolchain_startup_blocked,
14+
} from "../../utils/bundled_toolchain";
915

1016
/**
1117
* Current update status of the application
@@ -230,6 +236,28 @@ export function create_core_requests(
230236
};
231237
},
232238
get_tool_backend_paths: async () => ({ os: context.runtime_os, tools: TOOL_BACKEND_PATHS[context.runtime_os] }),
239+
get_bundled_toolchain_startup_block: async () => {
240+
const failures = bundled_toolchain_startup_failures();
241+
const blocked = bundled_toolchain_startup_blocked();
242+
const list: BundledToolchainFailure[] = failures ?? [];
243+
return { blocked, failures: list };
244+
},
245+
macos_open_full_disk_access_settings: async () => {
246+
if (process.platform !== "darwin") {
247+
return { ok: false, message: localization_t("runtime.full_disk_access_macos_only") };
248+
}
249+
try {
250+
const child = spawn(
251+
"open",
252+
["x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"],
253+
{ detached: true, stdio: "ignore" }
254+
);
255+
child.unref();
256+
return { ok: true, message: "" };
257+
} catch (error) {
258+
return { ok: false, message: String(error) };
259+
}
260+
},
233261
update_get_status: () => update_get_status_request(context),
234262
update_check: () => update_check_request(context),
235263
update_download: () => update_download_request(context),

src/bun/tools/utilities/documents/common/macos.ts

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { existsSync, readdirSync } from "node:fs";
21
import type { CommandResult } from "./types/common";
32
import { command_debug_log_finish, command_debug_log_start } from "../../../command-debug";
43
import { OperationCancelledError } from "../../../../helpers/operation_cancelled_error";
@@ -106,35 +105,9 @@ export async function documents_macos_command_succeeds(command: string[]): Promi
106105
}
107106
}
108107

109-
/** argv[0] for qpdf subprocesses (bundled binary). */
108+
/** argv[0] for qpdf subprocesses (bundled binary only; validated at startup). */
110109
export function documents_macos_qpdf_command(): string {
111-
const bundled_qpdf = bundled_cli_path("qpdf");
112-
const bundled_lib_dir = resolve_path_relative_to_qpdf_lib_dir(bundled_qpdf);
113-
if (bundled_lib_dir && has_qpdf_dylib_in_dir(bundled_lib_dir)) {
114-
return bundled_qpdf;
115-
}
116-
117-
const system_qpdf_candidates = ["/opt/homebrew/bin/qpdf", "/usr/local/bin/qpdf", "/usr/bin/qpdf"];
118-
for (const candidate of system_qpdf_candidates) {
119-
if (existsSync(candidate)) {
120-
return candidate;
121-
}
122-
}
123-
return bundled_qpdf;
124-
}
125-
126-
function resolve_path_relative_to_qpdf_lib_dir(qpdf_path: string): string | null {
127-
const lib_dir = `${qpdf_path}/../lib`;
128-
return existsSync(lib_dir) ? lib_dir : null;
129-
}
130-
131-
function has_qpdf_dylib_in_dir(dir_path: string): boolean {
132-
try {
133-
const entries = readdirSync(dir_path);
134-
return entries.some((entry) => entry.startsWith("libqpdf.") && entry.endsWith(".dylib"));
135-
} catch {
136-
return false;
137-
}
110+
return bundled_cli_path("qpdf");
138111
}
139112

140113
/**

src/bun/tools/utilities/video/yt-dlp/macos.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import type { AnalyzeResult, RunResult } from "./types/macos";
22
import { documents_macos_pick_directory } from "../../documents/common/macos";
33
import { yt_dlp_run_stream_command } from "./run_stream";
4-
import { yt_dlp_build_argv, yt_dlp_run_payload_parse } from "./run_validate";
4+
import {
5+
yt_dlp_build_argv,
6+
yt_dlp_run_payload_parse,
7+
yt_dlp_stderr_suggests_macos_safari_cookie_denial,
8+
} from "./run_validate";
59
import { bundled_cli_path } from "../../../../utils/bundled_cli_paths";
610
import { yt_dlp_analyze_run } from "./analyze";
711

@@ -43,11 +47,16 @@ export async function yt_dlp_macos_run_streaming(
4347
cwd: payload.output_directory,
4448
});
4549
const ok = result.exit_code === 0;
50+
const full_disk_access_hint =
51+
!ok &&
52+
payload.use_browser_cookies === true &&
53+
yt_dlp_stderr_suggests_macos_safari_cookie_denial(result.stderr_tail);
4654
return {
4755
ok,
4856
exit_code: result.exit_code,
4957
message: ok ? "Download finished." : `yt-dlp exited with code ${result.exit_code}.`,
5058
stdout_tail: result.stdout_tail,
5159
stderr_tail: result.stderr_tail,
60+
...(full_disk_access_hint ? { full_disk_access_hint: true as const } : {}),
5261
};
5362
}

0 commit comments

Comments
 (0)