Merge pull request #102 from delibae/fix/windows-project-creation-and-linux-segfault

fix: resolve Windows project creation failure (#101) and Linux segfault (#100)
This commit is contained in:
Hanjin Bae 2026-03-27 15:50:44 +09:00 committed by GitHub
commit 0f9755d7e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 158 additions and 47 deletions

View file

@ -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()
}

View file

@ -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",

View file

@ -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_*;
};

View file

@ -220,7 +220,7 @@ const CodeBlock: FC<{ language: string; code: string }> = ({
<span className="text-xs">
Run in{" "}
<code className="rounded bg-muted px-1 text-xs">
{projectRoot?.split("/").pop()}/
{projectRoot?.split(/[/\\]/).pop()}/
</code>
</span>
</div>

View file

@ -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 }) {
>
<PaperclipIcon className="size-3 shrink-0 text-muted-foreground/70" />
<span className="max-w-[140px] truncate text-foreground/80">
{path.split("/").pop()}
{path.split(/[/\\]/).pop()}
</span>
<button
onClick={() => handleRemoveAttachment(path)}
@ -447,7 +451,7 @@ function ScratchForm({ onBack }: { onBack: () => void }) {
</div>
{!locationOpen && projectFolder && projectName.trim() && (
<span className="min-w-0 max-w-[180px] truncate rounded-md bg-muted/40 px-2 py-0.5 font-mono text-[11px] text-muted-foreground/60">
.../{projectFolder.split("/").pop()}/{projectName.trim()}
.../{projectFolder.split(/[/\\]/).pop()}/{projectName.trim()}
</span>
)}
<ChevronDownIcon

View file

@ -2,7 +2,8 @@ import { useState, useCallback, useEffect, useRef } from "react";
import { open } from "@tauri-apps/plugin-dialog";
import { mkdir, writeTextFile } from "@tauri-apps/plugin-fs";
import { getCurrentWebview } from "@tauri-apps/api/webview";
import { documentDir } from "@tauri-apps/api/path";
import { homeDir } from "@tauri-apps/api/path";
import { toast } from "sonner";
import {
ChevronLeftIcon,
ChevronRightIcon,
@ -159,9 +160,15 @@ export function TemplatePreview() {
if (lastProjectFolder) {
setProjectFolder(lastProjectFolder);
} else {
documentDir()
.then((dir) => setProjectFolder(dir))
.catch(() => {});
homeDir()
.then((home) => join(home, "Documents", "ClaudePrism"))
.then(async (dir) => {
await mkdir(dir, { recursive: true }).catch(() => {});
setProjectFolder(dir);
})
.catch((err) =>
console.warn("Failed to resolve default project folder:", err),
);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
@ -400,7 +407,7 @@ export function TemplatePreview() {
try {
const projectPath = await join(projectFolder, projectName.trim());
await mkdir(projectPath, { recursive: true }).catch(() => {});
await mkdir(projectPath, { recursive: true });
const mainTexPath = await join(projectPath, template.mainFileName);
const mainExists = await exists(mainTexPath);
@ -418,12 +425,12 @@ export function TemplatePreview() {
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
@ -465,6 +472,9 @@ export function TemplatePreview() {
closePreview();
} 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);
}
@ -650,7 +660,7 @@ export function TemplatePreview() {
>
<PaperclipIcon className="size-3 shrink-0 text-muted-foreground/70" />
<span className="max-w-30 truncate text-foreground/80">
{path.split("/").pop()}
{path.split(/[/\\]/).pop()}
</span>
<button
onClick={() => handleRemoveAttachment(path)}
@ -713,7 +723,7 @@ export function TemplatePreview() {
</div>
{!locationOpen && projectFolder && projectName.trim() && (
<span className="min-w-0 max-w-35 truncate rounded-md bg-muted/40 px-2 py-0.5 font-mono text-[11px] text-muted-foreground/60">
.../{projectFolder.split("/").pop()}/
.../{projectFolder.split(/[/\\]/).pop()}/
{projectName.trim()}
</span>
)}

View file

@ -152,8 +152,7 @@ export function UvSetupDialog({ open, onClose }: UvSetupDialogProps) {
title={pythonPath}
>
Python:{" "}
{pythonPath.split("/").pop() ||
pythonPath.split("\\").pop()}
{pythonPath.split(/[/\\]/).pop()}
</div>
)}
</div>

View file

@ -344,7 +344,7 @@ function SnapshotRow({
{hasFiles && (
<div className="mt-0.5 truncate text-muted-foreground text-xs">
{snapshot.changed_files
.map((f) => f.split("/").pop())
.map((f) => f.split(/[/\\]/).pop())
.join(", ")}
</div>
)}

View file

@ -241,7 +241,7 @@ export const useDocumentStore = create<DocumentState>()((set, get) => ({
for (const f of fsFiles) {
const pf: ProjectFile = {
id: f.relativePath,
name: f.relativePath.split("/").pop() || f.relativePath,
name: f.relativePath.split(/[/\\]/).pop() || f.relativePath,
relativePath: f.relativePath,
absolutePath: f.absolutePath,
type: f.type,
@ -796,7 +796,7 @@ export const useDocumentStore = create<DocumentState>()((set, get) => ({
const newAbsPath = await join(state.projectRoot, newRelativePath);
await renameFileOnDisk(file.absolutePath, newAbsPath);
const newName = newRelativePath.split("/").pop() || file.name;
const newName = newRelativePath.split(/[/\\]/).pop() || file.name;
migratePdfBytesKey(fileId, newRelativePath);
set((s) => {
const compileErrorCache = migrateCacheKey(
@ -833,7 +833,7 @@ export const useDocumentStore = create<DocumentState>()((set, get) => ({
const state = get();
if (!state.projectRoot) return;
const folderName = folderPath.split("/").pop()!;
const folderName = folderPath.split(/[/\\]/).pop()!;
const newFolderPath = targetFolder
? `${targetFolder}/${folderName}`
: folderName;
@ -911,7 +911,7 @@ export const useDocumentStore = create<DocumentState>()((set, get) => ({
// New file on disk
const pf: ProjectFile = {
id: fsFile.relativePath,
name: fsFile.relativePath.split("/").pop() || fsFile.relativePath,
name: fsFile.relativePath.split(/[/\\]/).pop() || fsFile.relativePath,
relativePath: fsFile.relativePath,
absolutePath: fsFile.absolutePath,
type: fsFile.type,

View file

@ -26,7 +26,7 @@ export const useProjectStore = create<ProjectState>()(
setLastProjectFolder: (path) => set({ lastProjectFolder: path }),
addRecentProject: (path) => {
const name = path.split("/").pop() || path;
const name = path.split(/[/\\]/).pop() || path;
set((state) => {
const filtered = state.recentProjects.filter((p) => p.path !== path);
return {