Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 55 additions & 16 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -399,26 +399,53 @@ check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }

_python3_command() {
if command -v python3 >/dev/null 2>&1 &&
if [[ -n "${SPECKIT_PYTHON:-}" ]] && command -v "$SPECKIT_PYTHON" >/dev/null 2>&1 &&
"$SPECKIT_PYTHON" -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
printf '%s\n' "$SPECKIT_PYTHON"
elif command -v python3 >/dev/null 2>&1 &&
python3 -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
printf '%s\n' "python3"
elif command -v python >/dev/null 2>&1 &&
python -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
printf '%s\n' "python"
elif command -v py >/dev/null 2>&1 &&
py -3 -c 'import sys' >/dev/null 2>&1; then
printf '%s\n' "py -3"
printf '%s\n' "py" "-3"
else
return 1
fi
}

# SPECKIT_YAML_RUNTIME_FALLBACK=1
_python3_yaml_command() {
if [[ -n "${SPECKIT_PYTHON:-}" ]] && command -v "$SPECKIT_PYTHON" >/dev/null 2>&1 &&
PYTHONSAFEPATH=1 "$SPECKIT_PYTHON" -c 'import sys, yaml; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
printf '%s\n' "$SPECKIT_PYTHON"
elif command -v python3 >/dev/null 2>&1 &&
PYTHONSAFEPATH=1 python3 -c 'import sys, yaml; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
printf '%s\n' "python3"
elif command -v python >/dev/null 2>&1 &&
PYTHONSAFEPATH=1 python -c 'import sys, yaml; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
printf '%s\n' "python"
elif command -v py >/dev/null 2>&1 &&
PYTHONSAFEPATH=1 py -3 -c 'import sys, yaml; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
printf '%s\n' "py" "-3"
elif command -v uv >/dev/null 2>&1 &&
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
printf '%s\n' "uv" "run" "--isolated" "--no-project" "--with" "pyyaml==6.0.3" "python"
else
return 1
fi
}

_sorted_extension_ids() {
local ext_dir="$1"
local python_spec
if python_spec=$(_python3_command); then
local -a python_cmd
read -r -a python_cmd <<< "$python_spec"
local -a python_cmd=()
local _python_cmd_line
while IFS= read -r _python_cmd_line; do
python_cmd+=("$_python_cmd_line")
done < <(_python3_command)
if [ "${#python_cmd[@]}" -gt 0 ]; then
local py_stderr sorted_ids
py_stderr=$(mktemp)
if sorted_ids=$(SPECKIT_EXTENSIONS="$ext_dir" "${python_cmd[@]}" -c "
Expand Down Expand Up @@ -513,11 +540,11 @@ resolve_template() {
local presets_dir="$repo_root/.specify/presets"
if [ -d "$presets_dir" ]; then
local registry_file="$presets_dir/.registry"
local python_spec=""
local -a python_cmd=()
if python_spec=$(_python3_command); then
read -r -a python_cmd <<< "$python_spec"
fi
local _python_cmd_line
while IFS= read -r _python_cmd_line; do
python_cmd+=("$_python_cmd_line")
done < <(_python3_command)
if [ -f "$registry_file" ] && [ "${#python_cmd[@]}" -gt 0 ]; then
# Read preset IDs sorted by priority (lower number = higher precedence).
# The python3 call is wrapped in an if-condition so that set -e does not
Expand Down Expand Up @@ -636,11 +663,13 @@ resolve_template_content() {
local registry_file="$presets_dir/.registry"
local sorted_presets=""
local registry_parsed=false
local python_spec=""
local -a python_cmd=()
if python_spec=$(_python3_command); then
read -r -a python_cmd <<< "$python_spec"
fi
local -a yaml_cmd=()
local yaml_cmd_resolved=false
local _python_cmd_line
while IFS= read -r _python_cmd_line; do
python_cmd+=("$_python_cmd_line")
done < <(_python3_command)
if [ -f "$registry_file" ] && [ "${#python_cmd[@]}" -gt 0 ]; then
if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" "${python_cmd[@]}" -c "
import json, re, sys, os
Expand Down Expand Up @@ -682,15 +711,25 @@ except Exception:
local manifest="$presets_dir/$preset_id/preset.yml"
local manifest_declared=false
if [ -f "$manifest" ]; then
if [ "${#python_cmd[@]}" -eq 0 ]; then
if [ "$yaml_cmd_resolved" = false ]; then
while IFS= read -r _python_cmd_line; do
yaml_cmd+=("$_python_cmd_line")
done < <(_python3_yaml_command)
yaml_cmd_resolved=true
fi
if [ "${#yaml_cmd[@]}" -eq 0 ]; then
echo "Error: Python 3 and PyYAML are required to resolve preset template composition" >&2
return 2
fi
local result
local py_stderr
local parse_status
local -a yaml_env=("PYTHONSAFEPATH=1")
if [ "${yaml_cmd[0]}" = "uv" ]; then
yaml_env+=("PYTHONPATH=")
fi
py_stderr=$(mktemp)
if result=$(SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" "${python_cmd[@]}" -c "
if result=$(env "${yaml_env[@]}" PYTHONIOENCODING=utf-8 SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" "${yaml_cmd[@]}" -c "
import sys, os
try:
import yaml
Expand Down
97 changes: 89 additions & 8 deletions scripts/powershell/common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,10 @@ function Format-SpecKitCommand {
# Find a usable Python 3 executable (python3, python, or py -3).
# Returns the command/arguments as an array, or $null if none found.
function Get-Python3Command {
if ($env:SPECKIT_PYTHON -and (Get-Command $env:SPECKIT_PYTHON -ErrorAction SilentlyContinue)) {
$ver = & $env:SPECKIT_PYTHON --version 2>&1
if ($ver -match 'Python 3') { return @($env:SPECKIT_PYTHON) }
}
if (Get-Command python3 -ErrorAction SilentlyContinue) { return @('python3') }
if (Get-Command python -ErrorAction SilentlyContinue) {
$ver = & python --version 2>&1
Expand All @@ -333,6 +337,58 @@ function Get-Python3Command {
return $null
}

# SPECKIT_YAML_RUNTIME_FALLBACK=1
function Get-Python3WithYamlCommand {
$candidates = @()
if ($env:SPECKIT_PYTHON -and (Get-Command $env:SPECKIT_PYTHON -ErrorAction SilentlyContinue)) {
$candidates += ,@($env:SPECKIT_PYTHON)
}
if (Get-Command python3 -ErrorAction SilentlyContinue) { $candidates += ,@('python3') }
if (Get-Command python -ErrorAction SilentlyContinue) { $candidates += ,@('python') }
if (Get-Command py -ErrorAction SilentlyContinue) { $candidates += ,@('py', '-3') }
if (Get-Command uv -ErrorAction SilentlyContinue) {
$candidates += ,@(
'uv',
'run',
'--isolated',
'--no-project',
'--with',
'pyyaml==6.0.3',
'python'
)
}

$previousPythonPath = $env:PYTHONPATH
$previousPythonSafePath = $env:PYTHONSAFEPATH
try {
$env:PYTHONSAFEPATH = '1'
foreach ($command in $candidates) {
if ($command[0] -eq 'uv') {
Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
} elseif ($null -eq $previousPythonPath) {
Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
} else {
$env:PYTHONPATH = $previousPythonPath
}
[array]$commandArgs = if ($command.Count -gt 1) { $command[1..($command.Count - 1)] } else { @() }
& $command[0] @commandArgs -c 'import sys, yaml; raise SystemExit(sys.version_info.major != 3)' *> $null
if ($LASTEXITCODE -eq 0) { return $command }
}
} finally {
if ($null -eq $previousPythonPath) {
Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
} else {
$env:PYTHONPATH = $previousPythonPath
}
if ($null -eq $previousPythonSafePath) {
Remove-Item Env:PYTHONSAFEPATH -ErrorAction SilentlyContinue
} else {
$env:PYTHONSAFEPATH = $previousPythonSafePath
}
}
return $null
}

function Get-NormalizedPriority {
param($Value)

Expand Down Expand Up @@ -592,22 +648,34 @@ function Resolve-TemplateContent {
ForEach-Object { $_.Name }
}

$pyCmd = @(Get-Python3Command)
$yamlCmd = $null
$yamlCommandResolved = $false
foreach ($presetId in $sortedPresets) {
# Read strategy and file path from preset manifest
$strategy = 'replace'
$manifestFilePath = ''
$manifestDeclared = $false
$manifest = Join-Path $presetsDir "$presetId/preset.yml"
if ((Test-Path $manifest) -and -not $pyCmd) {
throw "Python 3 and PyYAML are required to resolve preset template composition"
}
if (Test-Path $manifest) {
if (-not $yamlCommandResolved) {
$yamlCmd = @(Get-Python3WithYamlCommand)
$yamlCommandResolved = $true
}
if (-not $yamlCmd) {
throw "Python 3 and PyYAML are required to resolve preset template composition"
}
try {
# Use Python to parse YAML manifest for strategy and file path
$pyArgs = if ($pyCmd.Count -gt 1) { $pyCmd[1..($pyCmd.Count-1)] } else { @() }
[array]$pyArgs = if ($yamlCmd.Count -gt 1) { $yamlCmd[1..($yamlCmd.Count-1)] } else { @() }
$pyStderrFile = [System.IO.Path]::GetTempFileName()
$stratResult = & $pyCmd[0] @pyArgs -c @"
$previousPythonPath = $env:PYTHONPATH
$previousPythonSafePath = $env:PYTHONSAFEPATH
try {
$env:PYTHONSAFEPATH = '1'
if ($yamlCmd[0] -eq 'uv') {
Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
}
$stratResult = & $yamlCmd[0] @pyArgs -c @"
import sys
try:
import yaml
Expand Down Expand Up @@ -662,8 +730,21 @@ except Exception as exc:
print(f'manifest_invalid: {exc}', file=sys.stderr)
sys.exit(3)
"@ $manifest $TemplateName 2>$pyStderrFile
if ($LASTEXITCODE -ne 0) {
if ($LASTEXITCODE -eq 2) {
$parserExitCode = $LASTEXITCODE
} finally {
if ($null -eq $previousPythonPath) {
Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
} else {
$env:PYTHONPATH = $previousPythonPath
}
if ($null -eq $previousPythonSafePath) {
Remove-Item Env:PYTHONSAFEPATH -ErrorAction SilentlyContinue
} else {
$env:PYTHONSAFEPATH = $previousPythonSafePath
}
}
if ($parserExitCode -ne 0) {
if ($parserExitCode -eq 2) {
throw "PyYAML is required to resolve preset template composition"
}
throw "Invalid preset manifest $manifest"
Expand Down
7 changes: 6 additions & 1 deletion scripts/powershell/resolve-template.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ if (-not $TemplateName) {
. "$PSScriptRoot/common.ps1"

$repoRoot = Get-RepoRoot
$templateContent = Resolve-TemplateContent -TemplateName $TemplateName -RepoRoot $repoRoot
try {
$templateContent = Resolve-TemplateContent -TemplateName $TemplateName -RepoRoot $repoRoot
} catch {
[Console]::Error.WriteLine("ERROR: $($_.Exception.Message)")
exit 1
}
if ($null -eq $templateContent) {
[Console]::Error.WriteLine("ERROR: Could not resolve required $TemplateName from the template override stack for $repoRoot")
exit 1
Expand Down
Loading