diff --git a/hooks/pre_use.lua b/hooks/pre_use.lua new file mode 100644 index 0000000..075aafc --- /dev/null +++ b/hooks/pre_use.lua @@ -0,0 +1,100 @@ +require("util") + +-- PreUse hook: Called before switching to a Python version +-- This ensures the target version is healthy and has correct shebang lines +function PLUGIN:PreUse(ctx) + -- Only process on Unix systems (Windows doesn't have shebang issues) + if OS_TYPE == "windows" then + return { + version = ctx.version + } + end + + local version = ctx.version + local previousVersion = ctx.previousVersion + local scope = ctx.scope + local cwd = ctx.cwd + local installedSdks = ctx.installedSdks + + print("Preparing Python " .. version .. " for use...") + + -- Find the target version in installed versions + local targetVersion = nil + for versionKey, sdkInfo in pairs(installedSdks) do + if versionKey == version then + targetVersion = sdkInfo + break + end + end + + if not targetVersion then + print("Warning: Target version " .. version .. " not found in installed versions") + return { + version = version + } + end + + local installPath = targetVersion.path + print("Checking Python installation at: " .. installPath) + + -- Perform health check + local healthReport = checkPythonHealth(installPath, version) + + -- Report health status and fix if needed + local needsRecheck = false + + if healthReport.overallHealth == "healthy" then + print("Python " .. version .. " is healthy and ready to use") + elseif healthReport.overallHealth == "warning" then + print("Python " .. version .. " has minor issues:") + for _, problem in ipairs(healthReport.problemsFound) do + print(" - " .. problem) + end + print("Attempting to fix issues...") + + -- Fix shebang issues + local success, fixedCount = fixShebangForVersion(installPath, version) + if success and fixedCount > 0 then + print("Fixed " .. fixedCount .. " shebang issues") + needsRecheck = true + end + elseif healthReport.overallHealth == "critical" then + print("Python " .. version .. " has critical issues:") + for _, problem in ipairs(healthReport.problemsFound) do + print(" - " .. problem) + end + + if #healthReport.problemsFound > 0 then + print("Attempting to fix critical issues...") + local success, fixedCount = fixShebangForVersion(installPath, version) + if success and fixedCount > 0 then + print("Fixed " .. fixedCount .. " critical issues") + needsRecheck = true + else + print("Some issues may require manual intervention") + end + end + end + + -- Only perform final health check if we made fixes + if needsRecheck then + print("Verifying fixes...") + local finalHealthReport = checkPythonHealth(installPath, version) + if finalHealthReport.overallHealth == "healthy" then + print("Python " .. version .. " is now ready for use") + else + print("Python " .. version .. " still has some issues but should be usable") + if #finalHealthReport.problemsFound > 0 then + print("Remaining issues:") + for _, problem in ipairs(finalHealthReport.problemsFound) do + print(" - " .. problem) + end + end + end + end + + -- Return the version in the correct format + return { + version = version + } +end diff --git a/lib/util.lua b/lib/util.lua index 2b46c4b..f0693b0 100644 --- a/lib/util.lua +++ b/lib/util.lua @@ -234,6 +234,10 @@ function linuxCompile(ctx) error("python build failed") end print("Build python success!") + + -- Fix shebang lines in Python scripts after successful build + fixShebangLines(path) + print("Cleaning up ...") status = os.execute("rm -rf " .. dest_pyenv_path) if status ~= 0 then @@ -354,10 +358,10 @@ function parseVersionFromPyenv() end local versions = jsonObj.versions; - + local numericVersions = {} local namedVersions = {} - + for _, version in ipairs(versions) do if string.match(version, "^%d") then table.insert(numericVersions, version) @@ -365,28 +369,305 @@ function parseVersionFromPyenv() table.insert(namedVersions, version) end end - + table.sort(numericVersions, function(a, b) return compare_versions(a, b) > 0 end) - + table.sort(namedVersions, function(a, b) return compare_versions(a, b) > 0 end) - + for _, version in ipairs(numericVersions) do table.insert(result, { version = version, note = "" }) end - + for _, version in ipairs(namedVersions) do table.insert(result, { version = version, note = "" }) end - + return result -end \ No newline at end of file +end + +-- Fix shebang lines in Python scripts that point to temporary directories +function fixShebangLines(installPath) + return fixShebangForVersion(installPath, nil) +end + +-- Fix shebang lines for a specific Python version installation +function fixShebangForVersion(installPath, version) + local versionInfo = version and (" for version " .. version) or "" + print("Fixing shebang lines in Python scripts" .. versionInfo .. "...") + + local binPath = installPath .. "/bin" + local pythonExecutable = installPath .. "/bin/python" + + -- Check if bin directory exists + local binDirCheck = io.open(binPath, "r") + if not binDirCheck then + print("No bin directory found at " .. binPath .. ", skipping shebang fix") + return false, 0 + end + binDirCheck:close() + + -- Use find command to get all files in bin directory (macOS compatible) + local findCmd = "find " .. binPath .. " -type f -perm +111 2>/dev/null" + local findResult = io.popen(findCmd) + if not findResult then + print("Could not scan bin directory, skipping shebang fix") + return false, 0 + end + + local fixedCount = 0 + local checkedCount = 0 + + -- Process each executable file + for filePath in findResult:lines() do + if filePath and filePath ~= "" then + checkedCount = checkedCount + 1 + -- Check if it's a Python script by examining the first line + local file = io.open(filePath, "r") + if file then + local firstLine = file:read("*l") + file:close() + + -- Check if it has a shebang line pointing to a temporary directory + if firstLine and firstLine:match("^#!/.*%.version%-fox/temp/[^/]+/") then + local filename = filePath:match("([^/]+)$") + print("Fixing shebang in: " .. filename) + if fixSingleShebang(filePath, pythonExecutable) then + fixedCount = fixedCount + 1 + end + end + end + end + end + + findResult:close() + print("Shebang fix completed" .. versionInfo .. ". Checked " .. checkedCount .. " files, fixed " .. fixedCount .. " files.") + return true, fixedCount +end + +-- Fix shebang line in a single file +function fixSingleShebang(filePath, newPythonPath) + -- Read the entire file + local file = io.open(filePath, "r") + if not file then + print("Warning: Could not open file for reading: " .. filePath) + return false + end + + local content = file:read("*all") + file:close() + + if not content or content == "" then + print("Warning: File is empty: " .. filePath) + return false + end + + -- Replace the shebang line - match any path containing .version-fox/temp/ + local newContent, replacements = content:gsub("^#!/[^\n]*%.version%-fox/temp/[^/]+/[^\n]*", "#!" .. newPythonPath) + + if replacements == 0 then + -- No replacement made, file might not have the problematic shebang + return false + end + + -- Create backup of original file + local backupPath = filePath .. ".bak" + local backupFile = io.open(backupPath, "w") + if backupFile then + backupFile:write(content) + backupFile:close() + end + + -- Write the file back + local file = io.open(filePath, "w") + if not file then + print("Warning: Could not open file for writing: " .. filePath) + return false + end + + file:write(newContent) + file:close() + + -- Preserve executable permissions + local chmodResult = os.execute("chmod +x " .. filePath) + if chmodResult ~= 0 then + print("Warning: Could not set executable permissions on: " .. filePath) + end + + -- Remove backup file if everything succeeded + os.remove(backupPath) + + return true +end + +-- Check Python installation health and return detailed status +function checkPythonHealth(installPath, version) + local versionInfo = version and (" " .. version) or "" + print("Checking Python" .. versionInfo .. " installation health...") + + local binPath = installPath .. "/bin" + local pythonExecutable = installPath .. "/bin/python" + + local healthReport = { + installPath = installPath, + version = version, + binPath = binPath, + pythonExecutable = pythonExecutable, + binDirExists = false, + pythonExists = false, + scriptsChecked = {}, + problemsFound = {}, + overallHealth = "unknown" + } + + -- Check if bin directory exists + local binDirCheck = io.open(binPath, "r") + if not binDirCheck then + healthReport.overallHealth = "critical" + table.insert(healthReport.problemsFound, "Bin directory not found: " .. binPath) + return healthReport + end + binDirCheck:close() + healthReport.binDirExists = true + + -- Check if Python executable exists + local pythonCheck = io.open(pythonExecutable, "r") + if not pythonCheck then + healthReport.overallHealth = "critical" + table.insert(healthReport.problemsFound, "Python executable not found: " .. pythonExecutable) + return healthReport + end + pythonCheck:close() + healthReport.pythonExists = true + + -- Check critical Python scripts + local criticalScripts = {"pip", "pip3", "easy_install"} + local problemCount = 0 + + for _, scriptName in ipairs(criticalScripts) do + local scriptPath = binPath .. "/" .. scriptName + local scriptInfo = { + name = scriptName, + path = scriptPath, + exists = false, + executable = false, + shebangOk = false, + shebangLine = "" + } + + -- Check if script exists + local scriptFile = io.open(scriptPath, "r") + if scriptFile then + scriptInfo.exists = true + local firstLine = scriptFile:read("*l") + scriptFile:close() + + if firstLine then + scriptInfo.shebangLine = firstLine + -- Check if shebang points to temporary directory + if firstLine:match("^#!/.*%.version%-fox/temp/[^/]+/") then + scriptInfo.shebangOk = false + problemCount = problemCount + 1 + table.insert(healthReport.problemsFound, scriptName .. " has problematic shebang: " .. firstLine) + else + scriptInfo.shebangOk = true + end + end + + -- Check if script is executable + local execCheck = os.execute("test -x " .. scriptPath .. " 2>/dev/null") + scriptInfo.executable = (execCheck == 0) + if not scriptInfo.executable then + problemCount = problemCount + 1 + table.insert(healthReport.problemsFound, scriptName .. " is not executable") + end + else + table.insert(healthReport.problemsFound, scriptName .. " not found") + end + + table.insert(healthReport.scriptsChecked, scriptInfo) + end + + -- Determine overall health + if problemCount == 0 then + healthReport.overallHealth = "healthy" + elseif problemCount <= 2 then + healthReport.overallHealth = "warning" + else + healthReport.overallHealth = "critical" + end + + return healthReport +end + +-- Fix shebang issues for all installed Python versions +function fixAllPythonVersions(sdkCachePath) + print("Starting batch fix for all Python installations...") + + if not sdkCachePath then + print("Error: SDK cache path not provided") + return false + end + + local pythonCachePath = sdkCachePath .. "/python" + + -- Check if Python cache directory exists + local pythonCacheCheck = io.open(pythonCachePath, "r") + if not pythonCacheCheck then + print("No Python installations found at: " .. pythonCachePath) + return false + end + pythonCacheCheck:close() + + -- Find all Python version directories + local findCmd = "find " .. pythonCachePath .. " -maxdepth 1 -type d -name 'v-*' 2>/dev/null" + local findResult = io.popen(findCmd) + if not findResult then + print("Could not scan Python installations directory") + return false + end + + local totalFixed = 0 + local versionsProcessed = 0 + + for versionPath in findResult:lines() do + if versionPath and versionPath ~= "" then + local version = versionPath:match("v%-(.+)$") + if version then + versionsProcessed = versionsProcessed + 1 + print("\n--- Processing Python " .. version .. " ---") + + -- Check health first + local healthReport = checkPythonHealth(versionPath, version) + + if healthReport.overallHealth == "healthy" then + print("Python " .. version .. " is healthy, skipping") + else + print("Python " .. version .. " has " .. #healthReport.problemsFound .. " issues, fixing...") + local success, fixedCount = fixShebangForVersion(versionPath, version) + if success then + totalFixed = totalFixed + fixedCount + end + end + end + end + end + + findResult:close() + + print("\n=== Batch Fix Summary ===") + print("Versions processed: " .. versionsProcessed) + print("Total files fixed: " .. totalFixed) + print("Batch fix completed!") + + return true +end