feat: split ClaudeAlt into independent package

This commit is contained in:
Александр
2026-07-25 01:18:17 +10:00
parent 1801be3549
commit f3af93802f
14 changed files with 361 additions and 256 deletions
+2 -252
View File
@@ -1,6 +1,6 @@
name='claude-desktop'
version=1.24012.0
release=4
release=5
summary='Claude desktop application'
summary_ru='Десктопное приложение Claude'
group='Development/Tools'
@@ -50,37 +50,25 @@ build_deps_altlinux=('binutils' 'python3')
sources=(
"https://downloads.claude.ai/claude-desktop/apt/stable/pool/main/c/claude-desktop/claude-desktop_${version}_amd64.deb?~archive=false&~name=claude-desktop.deb"
"local:///claude-alt"
"local:///claude-alt.png"
"local:///claude-tray-orange.png"
"local:///claude-alt-tray-turquoise.png"
'local:///LICENSE'
)
sources_arm64=(
"https://downloads.claude.ai/claude-desktop/apt/stable/pool/main/c/claude-desktop/claude-desktop_${version}_arm64.deb?~archive=false&~name=claude-desktop.deb"
"local:///claude-alt"
"local:///claude-alt.png"
"local:///claude-tray-orange.png"
"local:///claude-alt-tray-turquoise.png"
'local:///LICENSE'
)
checksums=(
'sha256:1096a8063956f8430fdbe504bb1c5e02985f31d0f45d082e4966e5d30ea7e3ac'
'sha256:00d9fe6549d82f564a99c6e8ec54d1380fdf8f0a20c99bb9b3cda1f689adf57d'
'sha256:f23de36a74c3f6ddae92511fc0dc659816853bf1d28520a6e0232fe2606fd0b4'
'sha256:be51081c0e0b2eec3489c25da38194dccf68fe7445928b32c30e1b8051eb557b'
'sha256:21d1db80c30921346252ed405fa36cb5c48c287afdd8ebe474073389fd88e85e'
'sha256:40efe7a2755d02c9fe13f98cae18705e2d31e5bbb24816ebbd4951c5e3a56b3f'
)
checksums_arm64=(
'sha256:37dd5dc1b0bfcb3192561acbd33cb59037806fe9b95221a3ddf38244203196f1'
'sha256:00d9fe6549d82f564a99c6e8ec54d1380fdf8f0a20c99bb9b3cda1f689adf57d'
'sha256:f23de36a74c3f6ddae92511fc0dc659816853bf1d28520a6e0232fe2606fd0b4'
'sha256:be51081c0e0b2eec3489c25da38194dccf68fe7445928b32c30e1b8051eb557b'
'sha256:21d1db80c30921346252ed405fa36cb5c48c287afdd8ebe474073389fd88e85e'
'sha256:40efe7a2755d02c9fe13f98cae18705e2d31e5bbb24816ebbd4951c5e3a56b3f'
)
@@ -111,208 +99,6 @@ package() {
"${pkgdir}/usr/lib/claude-desktop/resources/${icon_name}"
done
# ClaudeAlt получает отдельные executable и app.asar. Остальные неизменяемые
# Electron-файлы подключаются к основной копии относительными symlink-ами.
# Это сохраняет отдельный resourcesPath, но не удваивает пакет целиком.
install -dm755 \
"${pkgdir}/usr/lib/claude-alt" \
"${pkgdir}/usr/lib/claude-alt/resources"
cp -p "${pkgdir}/usr/lib/claude-desktop/claude-desktop" \
"${pkgdir}/usr/lib/claude-alt/claude-alt-bin"
for entry in "${pkgdir}/usr/lib/claude-desktop"/*; do
entry_name="${entry##*/}"
case "${entry_name}" in
claude-desktop | resources) continue ;;
esac
ln -s "../claude-desktop/${entry_name}" \
"${pkgdir}/usr/lib/claude-alt/${entry_name}"
done
for entry in "${pkgdir}/usr/lib/claude-desktop/resources"/*; do
entry_name="${entry##*/}"
case "${entry_name}" in
app.asar)
cp -p "${entry}" "${pkgdir}/usr/lib/claude-alt/resources/app.asar"
;;
icon.png | TrayIconLinux.png | TrayIconLinux-Dark.png)
# Эти ресурсы ниже заменяются иконкой ClaudeAlt.
;;
*)
ln -s "../../claude-desktop/resources/${entry_name}" \
"${pkgdir}/usr/lib/claude-alt/resources/${entry_name}"
;;
esac
done
# Большая иконка ClaudeAlt используется окном, а в tray устанавливается
# отдельный бирюзовый вариант штатного глифа Claude.
install -Dm644 "${srcdir}/claude-alt.png" \
"${pkgdir}/usr/lib/claude-alt/resources/icon.png"
for icon_name in TrayIconLinux.png TrayIconLinux-Dark.png; do
install -Dm644 "${srcdir}/claude-alt-tray-turquoise.png" \
"${pkgdir}/usr/lib/claude-alt/resources/${icon_name}"
done
alt_asar="${pkgdir}/usr/lib/claude-alt/resources/app.asar"
python3 - "${alt_asar}" <<'PY'
import copy
import hashlib
import json
import struct
import sys
from pathlib import Path
asar_path = Path(sys.argv[1])
data = asar_path.read_bytes()
if len(data) < 16:
raise SystemExit("ClaudeAlt app.asar is too small")
_, _, _, header_size = struct.unpack("<IIII", data[:16])
header = json.loads(data[16 : 16 + header_size])
base_offset = 16 + ((header_size + 3) & ~3)
output = bytearray()
patched_package_json = 0
patched_cowork_socket = 0
patched_user_agent = 0
def integrity_for(content: bytes, block_size: int = 4194304) -> dict:
blocks = [
hashlib.sha256(content[index : index + block_size]).hexdigest()
for index in range(0, len(content), block_size)
]
if not blocks:
blocks = [hashlib.sha256(b"").hexdigest()]
return {
"algorithm": "SHA256",
"hash": hashlib.sha256(content).hexdigest(),
"blockSize": block_size,
"blocks": blocks,
}
def rebuild_node(node: dict, prefix: str = "") -> dict:
global patched_cowork_socket, patched_package_json, patched_user_agent
rebuilt = {}
for name, child in node.get("files", {}).items():
path = f"{prefix}/{name}" if prefix else name
if "files" in child:
rebuilt[name] = {
**{key: value for key, value in child.items() if key != "files"},
"files": rebuild_node(child, path),
}
continue
entry = copy.deepcopy(child)
if entry.get("unpacked"):
rebuilt[name] = entry
continue
offset = int(entry["offset"])
size = int(entry["size"])
content = data[base_offset + offset : base_offset + offset + size]
content_changed = False
if path == "package.json":
package_json = json.loads(content)
expected = {
"productName": "Claude",
"desktopName": "com.anthropic.Claude.desktop",
}
for field, expected_value in expected.items():
if package_json.get(field) != expected_value:
raise SystemExit(
f"unexpected Claude package.json {field}: "
f"{package_json.get(field)!r} != {expected_value!r}"
)
# Отдельный productName нужен Electron для независимого instance
# lock. Официальный User-Agent восстанавливается ниже отдельно.
package_json["productName"] = "ClaudeAlt"
package_json["desktopName"] = "com.anthropic.ClaudeAlt.desktop"
content = (json.dumps(package_json, indent=2) + "\n").encode("utf-8")
patched_package_json += 1
content_changed = True
socket_name = b"claude-cowork-vm.sock"
socket_matches = content.count(socket_name)
if socket_matches:
content = content.replace(socket_name, b"claude-alt-cowork-vm.sock")
patched_cowork_socket += socket_matches
content_changed = True
user_agent_marker = (
b'P.app.isPackaged||P.app.setName("Claude");RRn();'
)
user_agent_matches = content.count(user_agent_marker)
if user_agent_matches:
content = content.replace(
user_agent_marker,
b'P.app.isPackaged||P.app.setName("Claude");'
b'P.app.userAgentFallback=P.app.userAgentFallback'
b'.replaceAll("ClaudeAlt","Claude");RRn();',
)
patched_user_agent += user_agent_matches
content_changed = True
entry["offset"] = str(len(output))
if content_changed:
entry["size"] = len(content)
entry["integrity"] = integrity_for(
content,
entry.get("integrity", {}).get("blockSize", 4194304),
)
output.extend(content)
rebuilt[name] = entry
return rebuilt
rebuilt_header = {"files": rebuild_node(header)}
if patched_package_json != 1:
raise SystemExit(
f"expected one ClaudeAlt package.json, got {patched_package_json}"
)
if patched_cowork_socket != 1:
raise SystemExit(
f"expected one ClaudeAlt Cowork socket, got {patched_cowork_socket}"
)
if patched_user_agent != 1:
raise SystemExit(
f"expected one ClaudeAlt User-Agent patch, got {patched_user_agent}"
)
header_bytes = json.dumps(rebuilt_header, separators=(",", ":")).encode("utf-8")
header_padded_size = (len(header_bytes) + 3) & ~3
patched_data = (
struct.pack(
"<IIII",
4,
header_padded_size + 8,
header_padded_size + 4,
len(header_bytes),
)
+ header_bytes
+ (b"\0" * (header_padded_size - len(header_bytes)))
+ bytes(output)
)
if patched_data.count(b"com.anthropic.ClaudeAlt.desktop") != 1:
raise SystemExit("ClaudeAlt desktopName was not written exactly once")
if patched_data.count(b'"productName": "ClaudeAlt"') != 1:
raise SystemExit("ClaudeAlt productName was not written exactly once")
if patched_data.count(b'.replaceAll("ClaudeAlt","Claude")') != 1:
raise SystemExit("ClaudeAlt official User-Agent patch is missing")
if patched_data.count(b"claude-alt-cowork-vm.sock") != 1:
raise SystemExit("ClaudeAlt Cowork socket was not written exactly once")
asar_path.write_bytes(patched_data)
PY
chmod 755 "${pkgdir}/usr/lib/claude-alt/claude-alt-bin"
install -Dm755 "${srcdir}/claude-alt" \
"${pkgdir}/usr/lib/claude-alt/claude-alt"
install -dm755 "${pkgdir}/usr/share"
cp -a "${srcdir}/extracted/usr/share/applications" "${pkgdir}/usr/share/"
cp -a "${srcdir}/extracted/usr/share/icons" "${pkgdir}/usr/share/"
@@ -327,42 +113,8 @@ PY
-e 's/^Categories=.*/Categories=Development;/' \
"${desktop_file}"
install -Dm644 "${srcdir}/claude-alt.png" \
"${pkgdir}/usr/share/icons/hicolor/512x512/apps/claude-alt.png"
install -Dm644 /dev/stdin "${pkgdir}/usr/share/applications/com.anthropic.ClaudeAlt.desktop" <<'EOF'
[Desktop Entry]
Name=ClaudeAlt
Name[ru]=ClaudeAlt
Comment=Independent secondary Claude desktop application
Comment[ru]=Независимое второе приложение Claude
GenericName=AI Assistant
Keywords=AI;Chat;Assistant;Claude;Code;LLM;
Exec=claude-alt %U
Icon=claude-alt
Type=Application
StartupNotify=true
StartupWMClass=com.anthropic.ClaudeAlt
SingleMainWindow=true
Categories=Development;
Actions=NewChat;NewCode;
[Desktop Action NewChat]
Name=New chat
Name[ru]=Новый чат
Exec=claude-alt claude://claude.ai/new
[Desktop Action NewCode]
Name=New Claude Code session
Name[ru]=Новая сессия Claude Code
Exec=claude-alt claude://code/new
EOF
install -dm755 "${pkgdir}/usr/bin"
ln -sfn /usr/lib/claude-desktop/claude-desktop "${pkgdir}/usr/bin/claude-desktop"
ln -sfn /usr/lib/claude-alt/claude-alt "${pkgdir}/usr/bin/claude-alt"
ln -sfn /usr/lib/claude-alt/claude-alt "${pkgdir}/usr/bin/claude-code-alt"
ln -sfn /usr/lib/claude-alt/claude-alt "${pkgdir}/usr/bin/claude-desktop-account2"
install -dm755 "${pkgdir}/usr/share/licenses/claude-desktop"
install-license "${srcdir}/LICENSE" "claude-desktop/LICENSE.nivora"
@@ -382,7 +134,5 @@ files() {
files-find-license
files-find \
"/usr/lib/claude-desktop/**/*" \
"/usr/lib/claude-alt/**/*" \
"/usr/share/icons/hicolor/*/apps/claude-desktop.png" \
"/usr/share/icons/hicolor/*/apps/claude-alt.png"
"/usr/share/icons/hicolor/*/apps/claude-desktop.png"
}