Ctrl K
ComunidadGuías…

scripts/fix-symlinks.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
/* global console */
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========

/**
 * Fix Python symlinks in venv directories
 *
 * Problem:
 * - venv/bin/python is a symlink pointing to absolute path on build machine
 * - Example: /Users/builder/.../cache/uv_python/cpython-3.10.19-.../bin/python3.10
 * - Points to cache which is excluded by electron-builder
 * - Symlinks break on user machines
 *
 * Solution:
 * - Remove broken absolute symlinks
 * - Create relative symlinks pointing to uv_python directory
 * - Works on any machine after packaging
 */

import { Buffer } from 'buffer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..');

/**
 * Find Python executable in uv_python directory
 */
function findPythonExecutable(uvPythonDir) {
  if (!fs.existsSync(uvPythonDir)) {
    return null;
  }

  try {
    const entries = fs.readdirSync(uvPythonDir, { withFileTypes: true });

    // Find cpython directory
    for (const entry of entries) {
      if (entry.isDirectory() && entry.name.startsWith('cpython-')) {
        const possiblePaths = [
          // Unix paths
          path.join(uvPythonDir, entry.name, 'bin', 'python3.10'),
          path.join(uvPythonDir, entry.name, 'bin', 'python'),
          path.join(uvPythonDir, entry.name, 'install', 'bin', 'python3.10'),
          path.join(uvPythonDir, entry.name, 'install', 'bin', 'python'),
          // Windows paths
          path.join(uvPythonDir, entry.name, 'Scripts', 'python.exe'),
          path.join(
            uvPythonDir,
            entry.name,
            'install',
            'Scripts',
            'python.exe'
          ),
          path.join(uvPythonDir, entry.name, 'python.exe'),
        ];

        for (const pythonPath of possiblePaths) {
          if (fs.existsSync(pythonPath)) {
            return {
              absolutePath: pythonPath,
              cpythonDir: entry.name,
              // Extract relative path from uv_python
              relativePath: path.relative(uvPythonDir, pythonPath),
            };
          }
        }
      }
    }
  } catch (error) {
    console.error(`Error searching for Python: ${error.message}`);
  }

  return null;
}

/**
 * Fix Python symlinks in a venv
 */
function fixVenvSymlinks(venvPath, venvName) {
  const binDir = path.join(venvPath, 'bin');
  const scriptsDir = path.join(venvPath, 'Scripts');

  // Windows uses Scripts directory and doesn't need symlink fixes
  if (fs.existsSync(scriptsDir) && !fs.existsSync(binDir)) {
    console.log(`\n📝 Processing ${venvName}`);
    console.log(
      `   ℹ️  Windows venv detected - skipping symlink fixes (not needed)`
    );
    return true;
  }

  if (!fs.existsSync(binDir)) {
    console.log(`⚠️  ${venvName} bin directory not found: ${binDir}`);
    return false;
  }

  // Find Python in uv_python
  const uvPythonDir = path.join(
    projectRoot,
    'resources',
    'prebuilt',
    'uv_python'
  );
  const pythonInfo = findPythonExecutable(uvPythonDir);

  if (!pythonInfo) {
    console.log(`⚠️  No Python executable found in uv_python`);
    return false;
  }

  console.log(`\n📝 Processing ${venvName}`);
  console.log(`   Found Python: ${pythonInfo.cpythonDir}`);

  // Python symlink names to fix
  const symlinkNames = ['python', 'python3', 'python3.10'];
  let fixedCount = 0;

  for (const symlinkName of symlinkNames) {
    const symlinkPath = path.join(binDir, symlinkName);

    try {
      // Check if symlink exists
      let needsFix = false;
      let currentTarget = null;
      let symlinkExists = false;

      // Use lstatSync to check if symlink exists (works for broken symlinks too)
      try {
        const stat = fs.lstatSync(symlinkPath);
        symlinkExists = stat.isSymbolicLink();
      } catch {
        // File/symlink doesn't exist at all
        symlinkExists = false;
      }

      if (symlinkExists) {
        try {
          currentTarget = fs.readlinkSync(symlinkPath);

          // Check if it's a broken symlink or points to wrong location
          if (
            path.isAbsolute(currentTarget) ||
            currentTarget.includes('cache')
          ) {
            needsFix = true;
            console.log(
              `   ❌ ${symlinkName}: broken symlink -> ${currentTarget}`
            );
          } else {
            console.log(
              `   ✓  ${symlinkName}: already fixed -> ${currentTarget}`
            );
            continue;
          }
        } catch (err) {
          // readlinkSync failed - symlink is broken
          needsFix = true;
          console.log(
            `   ❌ ${symlinkName}: broken symlink (target doesn't exist), ${err.message}`
          );
        }
      } else {
        needsFix = true;
        console.log(`   ➕ ${symlinkName}: missing, will create`);
      }

      if (needsFix) {
        // Remove existing symlink if it exists
        try {
          fs.lstatSync(symlinkPath);
          // If lstatSync succeeds, the file/symlink exists
          fs.unlinkSync(symlinkPath);
        } catch {
          // File doesn't exist, nothing to remove
        }

        // Calculate relative path from bin/ to the Python executable
        // Example: ../../uv_python/cpython-3.10.19-macos-aarch64-none/bin/python3.10
        const relativePath = path.relative(binDir, pythonInfo.absolutePath);

        // For secondary symlinks (python3, python3.10), just point to python
        let targetPath;
        if (symlinkName === 'python') {
          targetPath = relativePath;
        } else {
          targetPath = 'python';
        }

        // Create new relative symlink
        fs.symlinkSync(targetPath, symlinkPath);
        console.log(`   ✅ ${symlinkName} -> ${targetPath}`);
        fixedCount++;
      }
    } catch (error) {
      console.error(`   ❌ Failed to fix ${symlinkName}: ${error.message}`);
    }
  }

  return fixedCount > 0;
}

/**
 * Fix shebang lines in all executable scripts in venv/bin directory
 * Note: Windows doesn't use shebangs - it uses .exe wrappers instead
 */
function fixScriptShebangs(venvPath, venvName) {
  const binDir = path.join(venvPath, 'bin');
  const scriptsDir = path.join(venvPath, 'Scripts');

  // Windows uses Scripts directory and doesn't need shebang fixes
  if (fs.existsSync(scriptsDir) && !fs.existsSync(binDir)) {
    console.log(`\n📝 Processing ${venvName}`);
    console.log(
      `   ℹ️  Windows venv detected - skipping shebang fixes (not needed)`
    );
    return { fixed: 0, skipped: 0 };
  }

  if (!fs.existsSync(binDir)) {
    console.log(`⚠️  ${venvName} bin directory not found: ${binDir}`);
    return { fixed: 0, skipped: 0 };
  }

  console.log(`\n📝 Fixing shebangs in ${venvName}/bin scripts`);

  let fixedCount = 0;
  let skippedCount = 0;

  try {
    const entries = fs.readdirSync(binDir);

    for (const entry of entries) {
      const filePath = path.join(binDir, entry);

      // Skip directories and symlinks
      try {
        const stat = fs.lstatSync(filePath);
        if (stat.isDirectory() || stat.isSymbolicLink()) {
          continue;
        }
      } catch (err) {
        console.error(`   ❌ Failed to check ${entry}: ${err.message}`);
        continue;
      }

      try {
        // Read first line to check for shebang
        const fd = fs.openSync(filePath, 'r');
        const buffer = Buffer.alloc(512);
        const bytesRead = fs.readSync(fd, buffer, 0, 512, 0);
        fs.closeSync(fd);

        if (bytesRead === 0) continue;

        const content = buffer.toString('utf-8', 0, bytesRead);
        const firstLineEnd = content.indexOf('\n');
        if (firstLineEnd === -1) continue;

        const firstLine = content.substring(0, firstLineEnd);

        // Check if it's a Python shebang with absolute path
        if (!firstLine.startsWith('#!') || !firstLine.includes('python')) {
          continue;
        }

        // Check if it contains an absolute path to the venv
        // Support both Unix (/resources/prebuilt/venv/) and Windows (\resources\prebuilt\venv\ or \resources\prebuilt\Scripts\)
        const shebangPath = firstLine.substring(2).trim();
        const hasVenvPath =
          firstLine.includes('/resources/prebuilt/venv/') ||
          firstLine.includes('\\resources\\prebuilt\\venv\\') ||
          firstLine.includes('/resources/prebuilt/terminal_venv/') ||
          firstLine.includes('\\resources\\prebuilt\\terminal_venv\\') ||
          firstLine.includes('/resources/prebuilt/Scripts/') ||
          firstLine.includes('\\resources\\prebuilt\\Scripts\\');

        if (!path.isAbsolute(shebangPath) && !hasVenvPath) {
          skippedCount++;
          continue;
        }

        // Read full file content
        const fullContent = fs.readFileSync(filePath, 'utf-8');

        // Replace absolute shebang with placeholder
        // This will be replaced at runtime with actual venv path
        // Similar to how pyvenv.cfg is handled
        const newContent = fullContent.replace(
          /^#!.*python.*$/m,
          '#!{{PREBUILT_VENV_PYTHON}}'
        );

        if (newContent !== fullContent) {
          fs.writeFileSync(filePath, newContent, 'utf-8');
          // Preserve executable permissions
          fs.chmodSync(filePath, 0o755);
          fixedCount++;

          if (fixedCount <= 5) {
            // Only show first 5 for brevity
            console.log(`   ✅ Fixed: ${entry}`);
          }
        }
      } catch (err) {
        // Silently skip files that can't be processed
        console.error(`   ❌ Failed to process ${entry}: ${err.message}`);
        continue;
      }
    }

    if (fixedCount > 5) {
      console.log(`   ✅ ... and ${fixedCount - 5} more files`);
    }
    console.log(
      `   📊 Total: ${fixedCount} fixed, ${skippedCount} already correct`
    );
  } catch (error) {
    console.error(`❌ Error processing ${venvName}: ${error.message}`);
  }

  return { fixed: fixedCount, skipped: skippedCount };
}

/**
 * Main function
 */
function main() {
  console.log(
    '🔧 Fixing Python symlinks and placeholders in venv directories...'
  );
  console.log('==========================================================\n');

  const venvDirs = [
    {
      path: path.join(projectRoot, 'resources', 'prebuilt', 'venv'),
      name: 'backend venv',
    },
    {
      path: path.join(projectRoot, 'resources', 'prebuilt', 'terminal_venv'),
      name: 'terminal venv',
    },
  ];

  let symlinkSuccessCount = 0;
  let totalCount = 0;
  let totalShebangsFixed = 0;

  for (const { path: venvPath, name } of venvDirs) {
    if (fs.existsSync(venvPath)) {
      totalCount++;

      // Fix symlinks
      if (fixVenvSymlinks(venvPath, name)) {
        symlinkSuccessCount++;
      }

      // Fix shebangs in all scripts
      const { fixed } = fixScriptShebangs(venvPath, name);
      totalShebangsFixed += fixed;
    } else {
      console.log(`⚠️  ${name} directory not found: ${venvPath}`);
    }
  }

  console.log('\n==========================================================');
  if (totalCount === 0) {
    console.log(
      '⚠️  No venv directories found - this is OK for development builds'
    );
  } else {
    console.log(
      `✅ Fixed symlinks in ${symlinkSuccessCount}/${totalCount} venv(s)`
    );
    console.log(`✅ Fixed placeholders in ${totalShebangsFixed} script(s)`);
    console.log('✅ Venvs are now fully portable!');
  }
}

main();