From 9da4c110414b819de105d9a958bbbda4ba88973a Mon Sep 17 00:00:00 2001 From: vitjanz Date: Mon, 23 Feb 2026 14:20:50 +0100 Subject: [PATCH] Fixed installation scripts with correct encoding, added test script running for ps1 files. Adjusted file_utils to be compatible with windows. --- file_utils.py | 20 +++++++++++++++- install/powershell/examples.ps1 | 2 +- install/powershell/install.ps1 | 37 ++++++++++++++++++++++++++++-- install/powershell/walkthrough.ps1 | 2 +- render_machine/render_utils.py | 20 +++++++++++++--- 5 files changed, 73 insertions(+), 8 deletions(-) diff --git a/file_utils.py b/file_utils.py index 672c305..d392620 100644 --- a/file_utils.py +++ b/file_utils.py @@ -1,5 +1,6 @@ import os import shutil +import stat from pathlib import Path from liquid2 import Environment, FileSystemLoader, StrictUndefined @@ -94,7 +95,22 @@ def list_folders_in_directory(directory): # delete a folder and all its subfolders and files def delete_folder(folder_name): if os.path.exists(folder_name): - shutil.rmtree(folder_name) + # Use writable-aware deletion so read-only files (e.g. .git objects on Windows) don't cause PermissionError + delete_files_and_subfolders(folder_name) + _make_writable(folder_name) + os.rmdir(folder_name) + + +def _make_writable(path: str) -> None: + """On Windows, clear read-only so deletion can succeed.""" + # TODO - Check if this can be done in a cleaner way. + is_windows = os.name == "nt" + if is_windows: + try: + mode = os.stat(path).st_mode + os.chmod(path, mode | stat.S_IWRITE) + except OSError: + pass def delete_files_and_subfolders(directory): @@ -106,12 +122,14 @@ def delete_files_and_subfolders(directory): # Delete files for file in files: file_path = os.path.join(root, file) + _make_writable(file_path) os.remove(file_path) total_files_deleted += 1 # Delete directories for dir_ in dirs: dir_path = os.path.join(root, dir_) + _make_writable(dir_path) os.rmdir(dir_path) total_folders_deleted += 1 diff --git a/install/powershell/examples.ps1 b/install/powershell/examples.ps1 index dba90bd..68925ad 100644 --- a/install/powershell/examples.ps1 +++ b/install/powershell/examples.ps1 @@ -1,4 +1,4 @@ -$ErrorActionPreference = 'Stop' +$ErrorActionPreference = 'Stop' # Brand Colors (use exported colors if available, otherwise define them) if (-not $env:YELLOW) { $YELLOW = "$([char]27)[38;2;224;255;110m" } else { $YELLOW = $env:YELLOW } diff --git a/install/powershell/install.ps1 b/install/powershell/install.ps1 index 5c0644f..70471b1 100644 --- a/install/powershell/install.ps1 +++ b/install/powershell/install.ps1 @@ -60,14 +60,29 @@ if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { Write-Host "${GREEN}✓${NC} uv detected" Write-Host "" +try { + $uvOutput = uv tool list 2>$null +} catch { + $uvOutput = @() +} + # Install or upgrade codeplain using uv tool -$codeplainLine = @(uv tool list 2>$null) | Where-Object { $_ -match '^codeplain' } | Select-Object -First 1 +$codeplainLine = $uvOutput | Where-Object { $_ -match '^codeplain' } | Select-Object -First 1 if ($codeplainLine) { $currentVersion = ($codeplainLine -replace 'codeplain v', '').Trim() Write-Host "${GRAY}codeplain ${currentVersion} is already installed.${NC}" Write-Host "upgrading to latest version..." Write-Host "" - uv tool upgrade codeplain 2>&1 | Out-Null + + try { + $upgradeOutput = & uv tool upgrade codeplain 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "uv exited with code $LASTEXITCODE" + } + } catch { + Write-Host "${RED}Failed to upgrade codeplain.${NC}" + Write-Host $_ + } $newLine = @(uv tool list 2>$null) | Where-Object { $_ -match '^codeplain' } | Select-Object -First 1 $newVersion = ($newLine -replace 'codeplain v', '').Trim() if ($currentVersion -eq $newVersion) { @@ -83,6 +98,24 @@ if ($codeplainLine) { Write-Host "${GREEN}✓ codeplain installed successfully!${NC}" } +# Ensure uv tool bin directory is on user PATH permanently (so codeplain is available) +$uvBinDir = Join-Path $env:USERPROFILE '.local\bin' +$userPath = [Environment]::GetEnvironmentVariable('Path', 'User') +if ($userPath) { + $pathEntries = $userPath -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + if ($uvBinDir -notin $pathEntries) { + $newPath = ($userPath.TrimEnd(';') + ';' + $uvBinDir) + [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') + $env:Path = $uvBinDir + ';' + $env:Path + Write-Host "${GREEN}✓${NC} added $uvBinDir to your user PATH" + } +} else { + [Environment]::SetEnvironmentVariable('Path', $uvBinDir, 'User') + $env:Path = $uvBinDir + ';' + $env:Path + Write-Host "${GREEN}✓${NC} added $uvBinDir to your user PATH" +} +Write-Host "" + # Check if API key already exists $skipApiKeySetup = $false if ($env:CODEPLAIN_API_KEY) { diff --git a/install/powershell/walkthrough.ps1 b/install/powershell/walkthrough.ps1 index 8c5472a..21810cd 100644 --- a/install/powershell/walkthrough.ps1 +++ b/install/powershell/walkthrough.ps1 @@ -1,4 +1,4 @@ -$ErrorActionPreference = 'Stop' +$ErrorActionPreference = 'Stop' # Brand Colors (use exported colors if available, otherwise define them) if (-not $env:YELLOW) { $YELLOW = "$([char]27)[38;2;224;255;110m" } else { $YELLOW = $env:YELLOW } diff --git a/render_machine/render_utils.py b/render_machine/render_utils.py index 66513f5..5913e5a 100644 --- a/render_machine/render_utils.py +++ b/render_machine/render_utils.py @@ -1,4 +1,5 @@ import subprocess +import sys import tempfile import time from typing import Optional @@ -51,19 +52,30 @@ def execute_script( ) -> tuple[int, str, Optional[str]]: temp_file_path = None script_timeout = timeout if timeout is not None else SCRIPT_EXECUTION_TIMEOUT + + script_path = file_utils.add_current_path_if_no_path(script) + # On Windows, .ps1 files must be run via PowerShell, not as the executable + if sys.platform == "win32" and script_path.lower().endswith(".ps1"): + cmd = ["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script_path] + scripts_args + else: + cmd = [script_path] + scripts_args try: start_time = time.time() result = subprocess.run( - [file_utils.add_current_path_if_no_path(script)] + scripts_args, + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + encoding="utf-8", + errors="replace", timeout=script_timeout, ) elapsed_time = time.time() - start_time # Log the info about the script execution if verbose: - with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".script_output") as temp_file: + with tempfile.NamedTemporaryFile( + mode="w+", encoding="utf-8", delete=False, suffix=".script_output" + ) as temp_file: temp_file.write(f"\n═════════════════════════ {script_type} Script Output ═════════════════════════\n") temp_file.write(result.stdout) temp_file.write("\n══════════════════════════════════════════════════════════════════════\n") @@ -95,7 +107,9 @@ def execute_script( except subprocess.TimeoutExpired as e: # Store timeout output in a temporary file if verbose: - with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".script_timeout") as temp_file: + with tempfile.NamedTemporaryFile( + mode="w+", encoding="utf-8", delete=False, suffix=".script_timeout" + ) as temp_file: temp_file.write(f"{script_type} script {script} timed out after {script_timeout} seconds.") if e.stdout: decoded_output = e.stdout.decode("utf-8") if isinstance(e.stdout, bytes) else e.stdout