mirror of
https://github.com/aaddrick/claude-desktop-debian.git
synced 2026-05-17 08:36:35 +03:00
fix: use correct linux VM checksums in cowork manifest patch (#329)
The cowork manifest patch (Patch 4) copied win32 file entries as linux entries. Since Anthropic now publishes Linux-specific VM images with different content, the win32 checksums cause silent validation failures and startVM timeouts. Compute correct SHA-256 checksums for the linux CDN files and embed them in build.sh. Patch 4 now replaces win32 checksums with the linux values before injecting the manifest entry. Falls back to win32 values if linux checksums are empty. The check-claude-version CI workflow is extended to automatically recompute VM checksums when a new version is detected. This is non-blocking — if CDN files aren't published yet or computation fails, the rest of the workflow proceeds unaffected. Fixes #329 Co-Authored-By: Claude <claude@anthropic.com>
This commit is contained in:
140
.github/workflows/check-claude-version.yml
vendored
140
.github/workflows/check-claude-version.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y p7zip-full wget
|
||||
sudo apt-get install -y p7zip-full wget zstd
|
||||
pip install playwright requests
|
||||
playwright install chromium
|
||||
|
||||
@@ -171,11 +171,12 @@ jobs:
|
||||
|
||||
# Download to temp files first so a partial download doesn't
|
||||
# produce a valid-looking but wrong SRI hash.
|
||||
# Keep the AMD64 installer for VM checksum extraction later.
|
||||
amd64_tmp=$(mktemp)
|
||||
curl -fsSL -o "$amd64_tmp" "$AMD64_URL" || { echo "AMD64 download failed"; exit 1; }
|
||||
AMD64_HEX=$(sha256sum "$amd64_tmp" | awk '{print $1}')
|
||||
AMD64_HASH=$(echo "$AMD64_HEX" | xxd -r -p | base64 -w0)
|
||||
rm "$amd64_tmp"
|
||||
echo "amd64_installer=$amd64_tmp" >> $GITHUB_OUTPUT
|
||||
echo "amd64_sri=sha256-$AMD64_HASH" >> $GITHUB_OUTPUT
|
||||
echo "amd64_sha256=$AMD64_HEX" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -214,6 +215,141 @@ jobs:
|
||||
echo "Updated build.sh checksums:"
|
||||
grep "claude_exe_sha256=" build.sh
|
||||
|
||||
- name: Extract VM bundle SHA from installer
|
||||
if: steps.check_update.outputs.update_needed == 'true'
|
||||
id: vm_bundle
|
||||
run: |
|
||||
INSTALLER="${{ steps.nix_hashes.outputs.amd64_installer }}"
|
||||
if [ -z "$INSTALLER" ] || [ ! -f "$INSTALLER" ]; then
|
||||
echo "No installer available, skipping VM checksums"
|
||||
echo "bundle_sha=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
echo "Extracting installer to find bundle SHA..."
|
||||
|
||||
# Extract exe → nupkg → app.asar → index.js
|
||||
7z x -y -o"$WORK/exe" "$INSTALLER" > /dev/null 2>&1
|
||||
NUPKG=$(find "$WORK/exe" -name "AnthropicClaude-*.nupkg" | head -1)
|
||||
if [ -z "$NUPKG" ]; then
|
||||
echo "No nupkg found, skipping VM checksums"
|
||||
rm -rf "$WORK"
|
||||
echo "bundle_sha=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
7z x -y -o"$WORK/nupkg" "$NUPKG" > /dev/null 2>&1
|
||||
ASAR="$WORK/nupkg/lib/net45/resources/app.asar"
|
||||
if [ ! -f "$ASAR" ]; then
|
||||
echo "No app.asar found, skipping VM checksums"
|
||||
rm -rf "$WORK"
|
||||
echo "bundle_sha=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
npx --yes @electron/asar extract "$ASAR" "$WORK/app" 2>/dev/null
|
||||
INDEX_JS="$WORK/app/.vite/build/index.js"
|
||||
if [ ! -f "$INDEX_JS" ]; then
|
||||
echo "No index.js found, skipping VM checksums"
|
||||
rm -rf "$WORK"
|
||||
echo "bundle_sha=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract bundle SHA (40-char hex after sha: or sha:")
|
||||
BUNDLE_SHA=$(grep -oP 'sha\s*:\s*"([a-f0-9]{40})"' "$INDEX_JS" \
|
||||
| grep -oP '[a-f0-9]{40}' | head -1)
|
||||
|
||||
rm -rf "$WORK"
|
||||
rm -f "$INSTALLER"
|
||||
|
||||
if [ -z "$BUNDLE_SHA" ]; then
|
||||
echo "Could not extract bundle SHA, skipping VM checksums"
|
||||
echo "bundle_sha=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Bundle SHA: $BUNDLE_SHA"
|
||||
|
||||
# Validate CDN has linux files for this SHA
|
||||
HTTP_CODE=$(curl -sI -o /dev/null -w "%{http_code}" \
|
||||
"https://downloads.claude.ai/vms/linux/x64/$BUNDLE_SHA/vmlinuz.zst")
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo "Linux VM files not published yet (HTTP $HTTP_CODE), skipping"
|
||||
echo "bundle_sha=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Linux VM files confirmed on CDN"
|
||||
echo "bundle_sha=$BUNDLE_SHA" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Compute VM bundle checksums
|
||||
if: steps.check_update.outputs.update_needed == 'true' && steps.vm_bundle.outputs.bundle_sha != ''
|
||||
id: vm_checksums
|
||||
run: |
|
||||
SHA="${{ steps.vm_bundle.outputs.bundle_sha }}"
|
||||
BASE="https://downloads.claude.ai/vms/linux"
|
||||
|
||||
compute_checksum() {
|
||||
local url="$1" label="$2"
|
||||
local hash
|
||||
hash=$(set -o pipefail
|
||||
curl -fsSL --max-time 600 "$url" \
|
||||
| zstd -d \
|
||||
| sha256sum \
|
||||
| awk '{print $1}'
|
||||
) || { echo " FAILED: $label"; echo ""; return; }
|
||||
|
||||
if [[ ! $hash =~ ^[a-f0-9]{64}$ ]]; then
|
||||
echo " INVALID hash for $label: $hash"
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
echo " $label: $hash"
|
||||
echo "$hash"
|
||||
}
|
||||
|
||||
echo "Computing x64 checksums..."
|
||||
X64_ROOTFS=$(compute_checksum "$BASE/x64/$SHA/rootfs.vhdx.zst" "x64/rootfs.vhdx")
|
||||
X64_VMLINUZ=$(compute_checksum "$BASE/x64/$SHA/vmlinuz.zst" "x64/vmlinuz")
|
||||
X64_INITRD=$(compute_checksum "$BASE/x64/$SHA/initrd.zst" "x64/initrd")
|
||||
|
||||
echo "Computing arm64 checksums..."
|
||||
ARM64_ROOTFS=$(compute_checksum "$BASE/arm64/$SHA/rootfs.vhdx.zst" "arm64/rootfs.vhdx")
|
||||
ARM64_VMLINUZ=$(compute_checksum "$BASE/arm64/$SHA/vmlinuz.zst" "arm64/vmlinuz")
|
||||
ARM64_INITRD=$(compute_checksum "$BASE/arm64/$SHA/initrd.zst" "arm64/initrd")
|
||||
|
||||
echo "x64_rootfs=$X64_ROOTFS" >> $GITHUB_OUTPUT
|
||||
echo "x64_vmlinuz=$X64_VMLINUZ" >> $GITHUB_OUTPUT
|
||||
echo "x64_initrd=$X64_INITRD" >> $GITHUB_OUTPUT
|
||||
echo "arm64_rootfs=$ARM64_ROOTFS" >> $GITHUB_OUTPUT
|
||||
echo "arm64_vmlinuz=$ARM64_VMLINUZ" >> $GITHUB_OUTPUT
|
||||
echo "arm64_initrd=$ARM64_INITRD" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update build.sh VM checksums
|
||||
if: steps.check_update.outputs.update_needed == 'true' && steps.vm_bundle.outputs.bundle_sha != ''
|
||||
run: |
|
||||
update_vm_checksum() {
|
||||
local var="$1" value="$2"
|
||||
if [ -z "$value" ]; then
|
||||
echo " Skipping $var (empty)"
|
||||
return
|
||||
fi
|
||||
sed -i "s/${var}='[^']*'/${var}='${value}'/" build.sh
|
||||
}
|
||||
|
||||
echo "Updating build.sh VM checksums..."
|
||||
update_vm_checksum vm_checksum_x64_rootfs_vhdx "${{ steps.vm_checksums.outputs.x64_rootfs }}"
|
||||
update_vm_checksum vm_checksum_x64_vmlinuz "${{ steps.vm_checksums.outputs.x64_vmlinuz }}"
|
||||
update_vm_checksum vm_checksum_x64_initrd "${{ steps.vm_checksums.outputs.x64_initrd }}"
|
||||
update_vm_checksum vm_checksum_arm64_rootfs_vhdx "${{ steps.vm_checksums.outputs.arm64_rootfs }}"
|
||||
update_vm_checksum vm_checksum_arm64_vmlinuz "${{ steps.vm_checksums.outputs.arm64_vmlinuz }}"
|
||||
update_vm_checksum vm_checksum_arm64_initrd "${{ steps.vm_checksums.outputs.arm64_initrd }}"
|
||||
|
||||
echo "Verifying updates:"
|
||||
grep "vm_checksum_" build.sh
|
||||
|
||||
- name: Update Nix package
|
||||
if: steps.check_update.outputs.update_needed == 'true'
|
||||
run: |
|
||||
|
||||
99
build.sh
99
build.sh
@@ -124,6 +124,18 @@ detect_architecture() {
|
||||
|
||||
echo "Target Architecture: $architecture"
|
||||
section_footer 'Architecture Detection'
|
||||
|
||||
# VM bundle checksums — SHA-256 of decompressed linux CDN files.
|
||||
# Updated automatically by check-claude-version.yml when the
|
||||
# bundle SHA changes. Used in patch_cowork_linux() Patch 4 to
|
||||
# inject correct linux manifest entries. If empty, Patch 4 falls
|
||||
# back to copying win32 checksums (which may not match).
|
||||
vm_checksum_x64_rootfs_vhdx='a829fe446f24d5e49dcca7f4c61042cf79bc53197d06c582f3fac69282131410'
|
||||
vm_checksum_x64_vmlinuz='9ab0e4031fbdf90c5133a18c0ab399e9abc0a5935777ac5b29c1e26dba8b6596'
|
||||
vm_checksum_x64_initrd='032214290388d688790d7b169575a5f75693543b21b33869423713411c12bd6d'
|
||||
vm_checksum_arm64_rootfs_vhdx='d6eb347a6914839514c42b262baeba48df81517cd7fdfbeb0e435dd98cbb7105'
|
||||
vm_checksum_arm64_vmlinuz='ce876786f908390c65a8a58a442399a864875e13eae8da5e4ebc8c0255772515'
|
||||
vm_checksum_arm64_initrd='c75a1aff9719bf09527a8f9f055b3fb6e2c0633973efc8295cf07e46bb6333e1'
|
||||
}
|
||||
|
||||
detect_distro() {
|
||||
@@ -960,7 +972,14 @@ patch_cowork_linux() {
|
||||
# All complex patches are done via node to avoid shell escaping issues
|
||||
# with minified JavaScript. Uses unique string anchors and dynamic
|
||||
# variable extraction to be version-agnostic per CLAUDE.md guidelines.
|
||||
if ! INDEX_JS="$index_js" SVC_PATH="cowork-vm-service.js" node << 'COWORK_PATCH'
|
||||
if ! INDEX_JS="$index_js" SVC_PATH="cowork-vm-service.js" \
|
||||
VM_CS_X64_ROOTFS="$vm_checksum_x64_rootfs_vhdx" \
|
||||
VM_CS_X64_VMLINUZ="$vm_checksum_x64_vmlinuz" \
|
||||
VM_CS_X64_INITRD="$vm_checksum_x64_initrd" \
|
||||
VM_CS_ARM64_ROOTFS="$vm_checksum_arm64_rootfs_vhdx" \
|
||||
VM_CS_ARM64_VMLINUZ="$vm_checksum_arm64_vmlinuz" \
|
||||
VM_CS_ARM64_INITRD="$vm_checksum_arm64_initrd" \
|
||||
node << 'COWORK_PATCH'
|
||||
const fs = require('fs');
|
||||
const indexJs = process.env.INDEX_JS;
|
||||
let code = fs.readFileSync(indexJs, 'utf8');
|
||||
@@ -1091,35 +1110,70 @@ if (pipeMatch) {
|
||||
// ============================================================
|
||||
// Patch 4: Bundle manifest - add Linux entries to Ln.files
|
||||
// Anchor: find files:{darwin: near rootfs.img checksum pattern
|
||||
// Extracts the win32 file entries (rootfs.vhdx, vmlinuz, initrd
|
||||
// with checksums) and reuses them as linux entries so the app's
|
||||
// built-in download infrastructure fetches VM images for Linux.
|
||||
// Falls back to empty arrays if win32 extraction fails.
|
||||
// Extracts the win32 file entries (rootfs.vhdx, vmlinuz, initrd)
|
||||
// for structure (names, progressStart/End), then replaces the
|
||||
// checksums with correct linux values from build-time env vars.
|
||||
// Falls back to win32 checksums if linux checksums unavailable.
|
||||
// ============================================================
|
||||
const linuxChecksums = {
|
||||
x64: {
|
||||
'rootfs.vhdx': process.env.VM_CS_X64_ROOTFS || '',
|
||||
'vmlinuz': process.env.VM_CS_X64_VMLINUZ || '',
|
||||
'initrd': process.env.VM_CS_X64_INITRD || '',
|
||||
},
|
||||
arm64: {
|
||||
'rootfs.vhdx': process.env.VM_CS_ARM64_ROOTFS || '',
|
||||
'vmlinuz': process.env.VM_CS_ARM64_VMLINUZ || '',
|
||||
'initrd': process.env.VM_CS_ARM64_INITRD || '',
|
||||
},
|
||||
};
|
||||
|
||||
function replaceChecksums(archArray, arch) {
|
||||
const checksums = linuxChecksums[arch];
|
||||
const hasChecksums = Object.values(checksums).every(
|
||||
v => /^[a-f0-9]{64}$/.test(v));
|
||||
if (!hasChecksums) {
|
||||
console.log(` WARNING: Missing linux ${arch} checksums,` +
|
||||
' using win32 values as fallback');
|
||||
return archArray;
|
||||
}
|
||||
let result = archArray;
|
||||
for (const [name, hash] of Object.entries(checksums)) {
|
||||
// Match: checksum:"<64-hex>" near name:"<name>"
|
||||
// The checksum field follows the name field in each entry
|
||||
const nameIdx = result.indexOf('"' + name + '"');
|
||||
if (nameIdx === -1) continue;
|
||||
const afterName = result.substring(nameIdx);
|
||||
const csRe = /checksum\s*:\s*"([a-f0-9]{64})"/;
|
||||
const csMatch = afterName.match(csRe);
|
||||
if (csMatch) {
|
||||
result = result.substring(0, nameIdx) +
|
||||
afterName.replace(csMatch[0],
|
||||
'checksum:"' + hash + '"');
|
||||
}
|
||||
}
|
||||
console.log(` Replaced ${arch} checksums with linux values`);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!code.includes('"linux":{') && !code.includes("'linux':{") &&
|
||||
!code.includes('linux:{')) {
|
||||
// Find the manifest SHA (40-char hex near files:{)
|
||||
const shaRe = /sha\s*:\s*"([a-f0-9]{40})"/;
|
||||
const shaMatch = code.match(shaRe);
|
||||
if (shaMatch) {
|
||||
// Find 'files:' or 'files :' after the sha
|
||||
const shaIdx = code.indexOf(shaMatch[0]);
|
||||
const afterSha = code.indexOf('files', shaIdx);
|
||||
if (afterSha !== -1 && afterSha - shaIdx < 200) {
|
||||
// Find the opening brace of files object
|
||||
// Extract the full files:{...} block
|
||||
const filesBlock = extractBlock(code, afterSha, '{');
|
||||
if (filesBlock) {
|
||||
const filesEnd = code.indexOf(filesBlock, afterSha)
|
||||
+ filesBlock.length;
|
||||
|
||||
// Extract win32 x64 and arm64 arrays
|
||||
// Extract win32 x64 and arm64 arrays for structure
|
||||
let win32x64 = null;
|
||||
let win32arm64 = null;
|
||||
const win32Idx = filesBlock.indexOf('win32');
|
||||
if (win32Idx !== -1) {
|
||||
// Scope to win32:{...} to avoid matching
|
||||
// x64/arm64 from darwin or other platforms
|
||||
const win32Block =
|
||||
extractBlock(filesBlock, win32Idx, '{');
|
||||
if (win32Block) {
|
||||
@@ -1136,33 +1190,36 @@ if (!code.includes('"linux":{') && !code.includes("'linux':{") &&
|
||||
}
|
||||
}
|
||||
|
||||
// Build linux entry: use extracted win32 arrays, or
|
||||
// fall back to empty arrays (vacuous truth)
|
||||
// Build linux entries with correct checksums
|
||||
let linuxX64 = '[]';
|
||||
let linuxArm64 = '[]';
|
||||
if (win32x64 && win32x64.includes('name')) {
|
||||
linuxX64 = win32x64;
|
||||
console.log(' Extracted win32 x64 file entries for linux');
|
||||
linuxX64 = replaceChecksums(win32x64, 'x64');
|
||||
console.log(' Built linux x64 entries from' +
|
||||
' win32 structure');
|
||||
}
|
||||
if (win32arm64 && win32arm64.includes('name')) {
|
||||
linuxArm64 = win32arm64;
|
||||
console.log(' Extracted win32 arm64 file entries for linux');
|
||||
linuxArm64 = replaceChecksums(
|
||||
win32arm64, 'arm64');
|
||||
console.log(' Built linux arm64 entries from' +
|
||||
' win32 structure');
|
||||
}
|
||||
|
||||
// Insert linux entry before the closing } of files
|
||||
const insertPos = filesEnd - 1;
|
||||
const linuxEntry =
|
||||
',linux:{x64:' + linuxX64 +
|
||||
',arm64:' + linuxArm64 + '}';
|
||||
code = code.substring(0, insertPos) +
|
||||
linuxEntry + code.substring(insertPos);
|
||||
console.log(' Added Linux entries to bundle manifest');
|
||||
console.log(' Added Linux entries to bundle' +
|
||||
' manifest');
|
||||
patchCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!code.includes('linux:{x64:')) {
|
||||
console.log(' WARNING: Could not add Linux bundle manifest entries');
|
||||
console.log(' WARNING: Could not add Linux bundle' +
|
||||
' manifest entries');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user