Skip to content

Commit 4079265

Browse files
committed
fix(scripts): add isolated PyYAML runtime fallback
1 parent a7c379c commit 4079265

6 files changed

Lines changed: 454 additions & 104 deletions

File tree

scripts/bash/common.sh

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,7 @@ check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" |
400400

401401
_python3_command() {
402402
if [[ -n "${SPECKIT_PYTHON:-}" ]] && command -v "$SPECKIT_PYTHON" >/dev/null 2>&1 &&
403-
"$SPECKIT_PYTHON" -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1 &&
404-
"$SPECKIT_PYTHON" -c 'import yaml' >/dev/null 2>&1; then
403+
"$SPECKIT_PYTHON" -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
405404
printf '%s\n' "$SPECKIT_PYTHON"
406405
elif command -v python3 >/dev/null 2>&1 &&
407406
python3 -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
@@ -417,6 +416,28 @@ _python3_command() {
417416
fi
418417
}
419418

419+
# SPECKIT_YAML_RUNTIME_FALLBACK=1
420+
_python3_yaml_command() {
421+
if [[ -n "${SPECKIT_PYTHON:-}" ]] && command -v "$SPECKIT_PYTHON" >/dev/null 2>&1 &&
422+
PYTHONSAFEPATH=1 "$SPECKIT_PYTHON" -c 'import sys, yaml; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
423+
printf '%s\n' "$SPECKIT_PYTHON"
424+
elif command -v python3 >/dev/null 2>&1 &&
425+
PYTHONSAFEPATH=1 python3 -c 'import sys, yaml; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
426+
printf '%s\n' "python3"
427+
elif command -v python >/dev/null 2>&1 &&
428+
PYTHONSAFEPATH=1 python -c 'import sys, yaml; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
429+
printf '%s\n' "python"
430+
elif command -v py >/dev/null 2>&1 &&
431+
PYTHONSAFEPATH=1 py -3 -c 'import sys, yaml; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
432+
printf '%s\n' "py" "-3"
433+
elif command -v uv >/dev/null 2>&1 &&
434+
PYTHONPATH= PYTHONSAFEPATH=1 uv run --isolated --no-project --with pyyaml==6.0.3 python -c 'import sys, yaml; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
435+
printf '%s\n' "uv" "run" "--isolated" "--no-project" "--with" "pyyaml==6.0.3" "python"
436+
else
437+
return 1
438+
fi
439+
}
440+
420441
_sorted_extension_ids() {
421442
local ext_dir="$1"
422443
local -a python_cmd=()
@@ -643,6 +664,8 @@ resolve_template_content() {
643664
local sorted_presets=""
644665
local registry_parsed=false
645666
local -a python_cmd=()
667+
local -a yaml_cmd=()
668+
local yaml_cmd_resolved=false
646669
local _python_cmd_line
647670
while IFS= read -r _python_cmd_line; do
648671
python_cmd+=("$_python_cmd_line")
@@ -688,15 +711,25 @@ except Exception:
688711
local manifest="$presets_dir/$preset_id/preset.yml"
689712
local manifest_declared=false
690713
if [ -f "$manifest" ]; then
691-
if [ "${#python_cmd[@]}" -eq 0 ]; then
714+
if [ "$yaml_cmd_resolved" = false ]; then
715+
while IFS= read -r _python_cmd_line; do
716+
yaml_cmd+=("$_python_cmd_line")
717+
done < <(_python3_yaml_command)
718+
yaml_cmd_resolved=true
719+
fi
720+
if [ "${#yaml_cmd[@]}" -eq 0 ]; then
692721
echo "Error: Python 3 and PyYAML are required to resolve preset template composition" >&2
693722
return 2
694723
fi
695724
local result
696725
local py_stderr
697726
local parse_status
727+
local -a yaml_env=("PYTHONSAFEPATH=1")
728+
if [ "${yaml_cmd[0]}" = "uv" ]; then
729+
yaml_env+=("PYTHONPATH=")
730+
fi
698731
py_stderr=$(mktemp)
699-
if result=$(SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" "${python_cmd[@]}" -c "
732+
if result=$(env "${yaml_env[@]}" PYTHONIOENCODING=utf-8 SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" "${yaml_cmd[@]}" -c "
700733
import sys, os
701734
try:
702735
import yaml

scripts/powershell/common.ps1

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -322,10 +322,7 @@ function Format-SpecKitCommand {
322322
function Get-Python3Command {
323323
if ($env:SPECKIT_PYTHON -and (Get-Command $env:SPECKIT_PYTHON -ErrorAction SilentlyContinue)) {
324324
$ver = & $env:SPECKIT_PYTHON --version 2>&1
325-
if ($ver -match 'Python 3') {
326-
& $env:SPECKIT_PYTHON -c 'import yaml' *> $null
327-
if ($LASTEXITCODE -eq 0) { return @($env:SPECKIT_PYTHON) }
328-
}
325+
if ($ver -match 'Python 3') { return @($env:SPECKIT_PYTHON) }
329326
}
330327
if (Get-Command python3 -ErrorAction SilentlyContinue) { return @('python3') }
331328
if (Get-Command python -ErrorAction SilentlyContinue) {
@@ -339,6 +336,58 @@ function Get-Python3Command {
339336
return $null
340337
}
341338

339+
# SPECKIT_YAML_RUNTIME_FALLBACK=1
340+
function Get-Python3WithYamlCommand {
341+
$candidates = @()
342+
if ($env:SPECKIT_PYTHON -and (Get-Command $env:SPECKIT_PYTHON -ErrorAction SilentlyContinue)) {
343+
$candidates += ,@($env:SPECKIT_PYTHON)
344+
}
345+
if (Get-Command python3 -ErrorAction SilentlyContinue) { $candidates += ,@('python3') }
346+
if (Get-Command python -ErrorAction SilentlyContinue) { $candidates += ,@('python') }
347+
if (Get-Command py -ErrorAction SilentlyContinue) { $candidates += ,@('py', '-3') }
348+
if (Get-Command uv -ErrorAction SilentlyContinue) {
349+
$candidates += ,@(
350+
'uv',
351+
'run',
352+
'--isolated',
353+
'--no-project',
354+
'--with',
355+
'pyyaml==6.0.3',
356+
'python'
357+
)
358+
}
359+
360+
$previousPythonPath = $env:PYTHONPATH
361+
$previousPythonSafePath = $env:PYTHONSAFEPATH
362+
try {
363+
$env:PYTHONSAFEPATH = '1'
364+
foreach ($command in $candidates) {
365+
if ($command[0] -eq 'uv') {
366+
Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
367+
} elseif ($null -eq $previousPythonPath) {
368+
Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
369+
} else {
370+
$env:PYTHONPATH = $previousPythonPath
371+
}
372+
[array]$commandArgs = if ($command.Count -gt 1) { $command[1..($command.Count - 1)] } else { @() }
373+
& $command[0] @commandArgs -c 'import sys, yaml; raise SystemExit(sys.version_info.major != 3)' *> $null
374+
if ($LASTEXITCODE -eq 0) { return $command }
375+
}
376+
} finally {
377+
if ($null -eq $previousPythonPath) {
378+
Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
379+
} else {
380+
$env:PYTHONPATH = $previousPythonPath
381+
}
382+
if ($null -eq $previousPythonSafePath) {
383+
Remove-Item Env:PYTHONSAFEPATH -ErrorAction SilentlyContinue
384+
} else {
385+
$env:PYTHONSAFEPATH = $previousPythonSafePath
386+
}
387+
}
388+
return $null
389+
}
390+
342391
function Get-NormalizedPriority {
343392
param($Value)
344393

@@ -598,22 +647,34 @@ function Resolve-TemplateContent {
598647
ForEach-Object { $_.Name }
599648
}
600649

601-
$pyCmd = @(Get-Python3Command)
650+
$yamlCmd = $null
651+
$yamlCommandResolved = $false
602652
foreach ($presetId in $sortedPresets) {
603653
# Read strategy and file path from preset manifest
604654
$strategy = 'replace'
605655
$manifestFilePath = ''
606656
$manifestDeclared = $false
607657
$manifest = Join-Path $presetsDir "$presetId/preset.yml"
608-
if ((Test-Path $manifest) -and -not $pyCmd) {
609-
throw "Python 3 and PyYAML are required to resolve preset template composition"
610-
}
611658
if (Test-Path $manifest) {
659+
if (-not $yamlCommandResolved) {
660+
$yamlCmd = @(Get-Python3WithYamlCommand)
661+
$yamlCommandResolved = $true
662+
}
663+
if (-not $yamlCmd) {
664+
throw "Python 3 and PyYAML are required to resolve preset template composition"
665+
}
612666
try {
613667
# Use Python to parse YAML manifest for strategy and file path
614-
$pyArgs = if ($pyCmd.Count -gt 1) { $pyCmd[1..($pyCmd.Count-1)] } else { @() }
668+
[array]$pyArgs = if ($yamlCmd.Count -gt 1) { $yamlCmd[1..($yamlCmd.Count-1)] } else { @() }
615669
$pyStderrFile = [System.IO.Path]::GetTempFileName()
616-
$stratResult = & $pyCmd[0] @pyArgs -c @"
670+
$previousPythonPath = $env:PYTHONPATH
671+
$previousPythonSafePath = $env:PYTHONSAFEPATH
672+
try {
673+
$env:PYTHONSAFEPATH = '1'
674+
if ($yamlCmd[0] -eq 'uv') {
675+
Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
676+
}
677+
$stratResult = & $yamlCmd[0] @pyArgs -c @"
617678
import sys
618679
try:
619680
import yaml
@@ -668,8 +729,21 @@ except Exception as exc:
668729
print(f'manifest_invalid: {exc}', file=sys.stderr)
669730
sys.exit(3)
670731
"@ $manifest $TemplateName 2>$pyStderrFile
671-
if ($LASTEXITCODE -ne 0) {
672-
if ($LASTEXITCODE -eq 2) {
732+
$parserExitCode = $LASTEXITCODE
733+
} finally {
734+
if ($null -eq $previousPythonPath) {
735+
Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
736+
} else {
737+
$env:PYTHONPATH = $previousPythonPath
738+
}
739+
if ($null -eq $previousPythonSafePath) {
740+
Remove-Item Env:PYTHONSAFEPATH -ErrorAction SilentlyContinue
741+
} else {
742+
$env:PYTHONSAFEPATH = $previousPythonSafePath
743+
}
744+
}
745+
if ($parserExitCode -ne 0) {
746+
if ($parserExitCode -eq 2) {
673747
throw "PyYAML is required to resolve preset template composition"
674748
}
675749
throw "Invalid preset manifest $manifest"

scripts/powershell/resolve-template.ps1

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ if (-not $TemplateName) {
2222
. "$PSScriptRoot/common.ps1"
2323

2424
$repoRoot = Get-RepoRoot
25-
$templateContent = Resolve-TemplateContent -TemplateName $TemplateName -RepoRoot $repoRoot
25+
try {
26+
$templateContent = Resolve-TemplateContent -TemplateName $TemplateName -RepoRoot $repoRoot
27+
} catch {
28+
[Console]::Error.WriteLine("ERROR: $($_.Exception.Message)")
29+
exit 1
30+
}
2631
if ($null -eq $templateContent) {
2732
[Console]::Error.WriteLine("ERROR: Could not resolve required $TemplateName from the template override stack for $repoRoot")
2833
exit 1

0 commit comments

Comments
 (0)