From 37957aacb8f5c1120b8484e07fb740b6a81cf320 Mon Sep 17 00:00:00 2001 From: delibae Date: Fri, 27 Mar 2026 15:49:55 +0900 Subject: [PATCH] fix: resolve Windows project creation failure and Linux segfault MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows (#101): Add explicit FS scopes to each permission in default.json so Tauri's scope validator accepts Windows paths. Remove silent .catch(() => {}) on mkdir/writeTextFile so errors surface via toast instead of failing silently. Replace all .split("/").pop() with .split(/[/\\]/).pop() (14 occurrences) for correct path parsing on Windows. Linux (#100): Add ELF linker version script (symbols.map) that hides statically linked ICU/HarfBuzz/FreeType/Fontconfig/Graphite2 symbols from the dynamic symbol table. This prevents symbol collisions with WebKit2GTK's own dynamic copies of these libraries, which caused a segfault at startup. The static linking introduced in v1.0.10 to fix #91 is preserved—ICU remains embedded in the binary so it works across distros. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/desktop/src-tauri/build.rs | 15 ++++ .../src-tauri/capabilities/default.json | 75 +++++++++++++------ apps/desktop/src-tauri/symbols.map | 50 +++++++++++++ .../claude-chat/markdown-renderer.tsx | 2 +- .../desktop/src/components/project-wizard.tsx | 20 +++-- .../template-gallery/template-preview.tsx | 28 ++++--- apps/desktop/src/components/uv-setup.tsx | 3 +- .../components/workspace/history-panel.tsx | 2 +- apps/desktop/src/stores/document-store.ts | 8 +- apps/desktop/src/stores/project-store.ts | 2 +- 10 files changed, 158 insertions(+), 47 deletions(-) create mode 100644 apps/desktop/src-tauri/symbols.map diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs index 2f2429d..0b99e58 100644 --- a/apps/desktop/src-tauri/build.rs +++ b/apps/desktop/src-tauri/build.rs @@ -6,5 +6,20 @@ fn main() { println!("cargo:rustc-env={key}={val}"); } } + + // On Linux, apply a version script to hide statically linked ICU/HarfBuzz/ + // FreeType/Fontconfig symbols from the dynamic symbol table. This prevents + // symbol collisions with the system copies loaded by WebKit2GTK (segfault). + // See: https://github.com/delibae/claude-prism/issues/100 + #[cfg(target_os = "linux")] + { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"); + println!( + "cargo:rustc-link-arg=-Wl,--version-script={}/symbols.map", + manifest_dir + ); + println!("cargo:rerun-if-changed=symbols.map"); + } + tauri_build::build() } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 55457d0..4b93ce6 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -21,28 +21,61 @@ "dialog:allow-message", "dialog:allow-ask", "fs:default", - "fs:allow-read", - "fs:allow-write", - "fs:allow-exists", - "fs:allow-mkdir", - "fs:allow-remove", - "fs:allow-rename", - "fs:allow-copy-file", - "fs:allow-read-dir", - "fs:allow-read-text-file", - "fs:allow-write-text-file", - "fs:allow-read-file", - "fs:allow-write-file", - "fs:allow-stat", - "fs:allow-watch", { - "identifier": "fs:scope", - "allow": [ - { "path": "$HOME/**" }, - { "path": "$DOCUMENT/**" }, - { "path": "$DESKTOP/**" }, - { "path": "$TEMP/**" } - ] + "identifier": "fs:allow-read", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-write", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-exists", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-mkdir", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-remove", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-rename", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-copy-file", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-read-dir", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-read-text-file", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-write-text-file", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-read-file", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-write-file", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-stat", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] + }, + { + "identifier": "fs:allow-watch", + "allow": [{ "path": "$HOME/**" }, { "path": "$DOCUMENT/**" }, { "path": "$DESKTOP/**" }, { "path": "$TEMP/**" }] }, "shell:default", "shell:allow-open", diff --git a/apps/desktop/src-tauri/symbols.map b/apps/desktop/src-tauri/symbols.map new file mode 100644 index 0000000..32a364c --- /dev/null +++ b/apps/desktop/src-tauri/symbols.map @@ -0,0 +1,50 @@ +/* Version script to hide statically linked library symbols. + * + * Prevents symbol collisions between our statically linked + * ICU/HarfBuzz/FreeType/Fontconfig (from vcpkg, used by Tectonic) + * and the dynamically linked versions used by WebKit2GTK at runtime. + * + * Without this, the ELF dynamic linker resolves WebKit2GTK's ICU + * calls to our static symbols (different version), causing a segfault + * at startup. Fixes #100; preserves #91 fix (static ICU = no + * libicuuc.so.70 not-found error on newer distros). + */ +{ + local: + /* ICU4C */ + u_*; + ucnv_*; + ucol_*; + ures_*; + udata_*; + ucal_*; + udat_*; + unum_*; + uset_*; + ustr_*; + ubidi_*; + ubrk_*; + uidna_*; + uloc_*; + unorm_*; + unorm2_*; + utrans_*; + icu_*; + UCNV_*; + + /* HarfBuzz */ + hb_*; + + /* FreeType */ + FT_*; + ft_*; + + /* Fontconfig */ + Fc*; + + /* Graphite2 */ + gr_*; + + /* libpng (often pulled in by FreeType) */ + png_*; +}; diff --git a/apps/desktop/src/components/claude-chat/markdown-renderer.tsx b/apps/desktop/src/components/claude-chat/markdown-renderer.tsx index bc4d83b..4c08078 100644 --- a/apps/desktop/src/components/claude-chat/markdown-renderer.tsx +++ b/apps/desktop/src/components/claude-chat/markdown-renderer.tsx @@ -220,7 +220,7 @@ const CodeBlock: FC<{ language: string; code: string }> = ({ Run in{" "} - {projectRoot?.split("/").pop()}/ + {projectRoot?.split(/[/\\]/).pop()}/ diff --git a/apps/desktop/src/components/project-wizard.tsx b/apps/desktop/src/components/project-wizard.tsx index f583381..e318349 100644 --- a/apps/desktop/src/components/project-wizard.tsx +++ b/apps/desktop/src/components/project-wizard.tsx @@ -3,6 +3,7 @@ import { open } from "@tauri-apps/plugin-dialog"; import { mkdir, writeTextFile } from "@tauri-apps/plugin-fs"; import { getCurrentWebview } from "@tauri-apps/api/webview"; import { homeDir } from "@tauri-apps/api/path"; +import { toast } from "sonner"; import { ArrowLeftIcon, FolderOpenIcon, @@ -133,11 +134,11 @@ function ScratchForm({ onBack }: { onBack: () => void }) { } else { homeDir() .then((home) => join(home, "Documents", "ClaudePrism")) - .then((dir) => { - mkdir(dir, { recursive: true }).catch(() => {}); + .then(async (dir) => { + await mkdir(dir, { recursive: true }).catch(() => {}); setProjectFolder(dir); }) - .catch(() => {}); + .catch((err) => console.warn("Failed to resolve default project folder:", err)); } }, []); // eslint-disable-line react-hooks/exhaustive-deps @@ -234,7 +235,7 @@ function ScratchForm({ onBack }: { onBack: () => void }) { try { const projectPath = await join(projectFolder, projectName.trim()); - await mkdir(projectPath, { recursive: true }).catch(() => {}); + await mkdir(projectPath, { recursive: true }); // Create CLAUDE.md for Claude Code context const claudeMdPath = await join(projectPath, "CLAUDE.md"); @@ -259,12 +260,12 @@ function ScratchForm({ onBack }: { onBack: () => void }) { if (attachments.length > 0) { const attachmentsDir = await join(projectPath, "attachments"); - await mkdir(attachmentsDir, { recursive: true }).catch(() => {}); + await mkdir(attachmentsDir, { recursive: true }); } if (purpose.trim()) { const attachmentNames = attachments - .map((p) => p.split("/").pop()) + .map((p) => p.split(/[/\\]/).pop()) .filter(Boolean); const attachmentSection = attachmentNames.length > 0 @@ -303,6 +304,9 @@ function ScratchForm({ onBack }: { onBack: () => void }) { } } catch (err) { console.error("Failed to create project:", err); + toast.error("Failed to create project", { + description: err instanceof Error ? err.message : String(err), + }); } finally { setIsCreating(false); } @@ -383,7 +387,7 @@ function ScratchForm({ onBack }: { onBack: () => void }) { > - {path.split("/").pop()} + {path.split(/[/\\]/).pop()}