feat: PowerShell 7 preflight and launcher - preflight.ps1, wikitool.ps1, doctor policy and Mark of the Web checks, pwsh CI job (#151, B)
Files changed: - .gitea/workflows/ci.yml - CHANGES.md - INSTALL.md - README.md - VERSION - docs/why-gates-are-code.md - instructions/bootstrap.md - instructions/bug-report.md - instructions/preflight.md - instructions/setup-instance.md - instructions/upgrade-instance.md - tools/CONTRACT.md - tools/README.md - tools/bugreport.py - tools/chemenu/cli.py - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/doctor.py - tools/chemenu/prerequisites.py - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_bugreport.py - tools/chemenu/tests/test_doctor.py - tools/chemenu/tests/test_preflight.py - tools/chemenu/tests/test_preflight_pwsh.py - tools/chemenu/toolpaths.py - tools/preflight.ps1 - tools/wikitool - tools/wikitool.ps1
This commit is contained in:
1 parent
4035b1ba12
commit
210e0c8286
27 files changed
+1513
-32
No files matched your search
@@ -0,0 +1,705 @@
|
||||
#Requires -Version 7
|
||||
# Preflight: check that this machine has what the stack needs, record where each
|
||||
# tool lives, and set up tools/.venv - before any `wikitool` command can run.
|
||||
#
|
||||
# pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1
|
||||
# pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1 --set rg=C:\Tools\rg.exe
|
||||
#
|
||||
# Always start it that way (instructions/preflight.md): the bypass holds for this one
|
||||
# process only and changes no setting, and it is what lets a script that carries a
|
||||
# Mark of the Web start at all, so that it can report its own mark.
|
||||
#
|
||||
# Exit 0: everything is in place and .wikitool-tools.json is complete.
|
||||
# Exit 42: the user has to act. The output says what, why, the command that fixes
|
||||
# it and what happens next; show it verbatim and wait (AGENTS.md, Tool
|
||||
# error contract). Never install anything on the user's behalf.
|
||||
# Exit 1: this script was called wrongly, or the tree next to it is incomplete.
|
||||
#
|
||||
# The counterpart of tools/preflight.sh, and the two answer the same questions from the
|
||||
# same list, tools/prerequisites.txt. What only this one checks: the PowerShell execution
|
||||
# policy and a Mark of the Web on the stack's own scripts - the two things that stop
|
||||
# tools/wikitool.ps1 from starting, and that exist only here. Windows PowerShell 5.1 is
|
||||
# not supported; `#Requires` turns it away.
|
||||
#
|
||||
# Four variables exist for the test suite and are read nowhere else:
|
||||
# CHEMENU_PREFLIGHT_PLATFORM (windows|macos|linux), CHEMENU_PREFLIGHT_LONGPATHS (0|1),
|
||||
# CHEMENU_PREFLIGHT_POLICY (`Scope=Policy,...`, as `Get-ExecutionPolicy -List` names them)
|
||||
# and CHEMENU_PREFLIGHT_MARKED (comma-separated script names below tools/) stand in for
|
||||
# the operating system, the registry, the policy and the file streams.
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$PSNativeCommandArgumentPassing = 'Standard'
|
||||
|
||||
$Dir = $PSScriptRoot
|
||||
$Root = Split-Path -Parent $Dir
|
||||
$Manifest = Join-Path $Dir 'prerequisites.txt'
|
||||
$ToolsFile = Join-Path $Root '.wikitool-tools.json'
|
||||
$Venv = Join-Path $Dir '.venv'
|
||||
$Requirements = Join-Path $Dir 'requirements.txt'
|
||||
$Stamp = Join-Path $Venv '.chemenu-requirements.sha256'
|
||||
$Self = 'pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1'
|
||||
|
||||
$PyProbe = "import sys; print(str(sys.version_info[0]) + '.' + str(sys.version_info[1])); print(sys.executable)"
|
||||
|
||||
function Write-Line {
|
||||
param([string]$Text = '')
|
||||
[Console]::Out.WriteLine($Text)
|
||||
}
|
||||
|
||||
function Write-ErrorLine {
|
||||
param([string]$Text)
|
||||
[Console]::Error.WriteLine($Text)
|
||||
}
|
||||
|
||||
# --- arguments ----------------------------------------------------------------
|
||||
|
||||
$Sets = @{}
|
||||
$index = 0
|
||||
while ($index -lt $args.Count) {
|
||||
$arg = [string]$args[$index]
|
||||
$given = $null
|
||||
if ($arg -eq '--set') {
|
||||
if ($index + 1 -ge $args.Count) {
|
||||
Write-ErrorLine 'preflight: --set needs <tool>=<path>'
|
||||
exit 1
|
||||
}
|
||||
$index++
|
||||
$given = [string]$args[$index]
|
||||
} elseif ($arg.StartsWith('--set=')) {
|
||||
$given = $arg.Substring(6)
|
||||
} elseif ($arg -eq '-h' -or $arg -eq '--help') {
|
||||
Write-Line "usage: $Self [--set <tool>=<path>]..."
|
||||
Write-Line ''
|
||||
Write-Line 'Checks the tools this stack needs (tools/prerequisites.txt), records their paths'
|
||||
Write-Line 'in .wikitool-tools.json and sets up tools/.venv. Exit 0 means ready; exit 42'
|
||||
Write-Line 'means the user has to act - the output says how.'
|
||||
exit 0
|
||||
} else {
|
||||
Write-ErrorLine "preflight: unknown argument: $arg"
|
||||
exit 1
|
||||
}
|
||||
$pivot = $given.IndexOf('=')
|
||||
if ($pivot -lt 1) {
|
||||
Write-ErrorLine "preflight: --set needs <tool>=<path>, got '$given'"
|
||||
exit 1
|
||||
}
|
||||
$Sets[$given.Substring(0, $pivot)] = $given.Substring($pivot + 1)
|
||||
$index++
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Manifest -PathType Leaf)) {
|
||||
Write-ErrorLine "preflight: $Manifest is missing - this script has to run from inside an unpacked stack tree."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- platform -----------------------------------------------------------------
|
||||
|
||||
if ($env:CHEMENU_PREFLIGHT_PLATFORM) {
|
||||
$Platform = $env:CHEMENU_PREFLIGHT_PLATFORM
|
||||
} elseif ($IsWindows) {
|
||||
$Platform = 'windows'
|
||||
} elseif ($IsMacOS) {
|
||||
$Platform = 'macos'
|
||||
} else {
|
||||
$Platform = 'linux'
|
||||
}
|
||||
|
||||
# --- problems and the guidance block --------------------------------------------
|
||||
|
||||
$script:ProblemCount = 0
|
||||
$script:Guide = ''
|
||||
$script:BadSet = $false
|
||||
|
||||
function Add-Problem {
|
||||
param([string]$What, [string]$Why, [string]$Fix)
|
||||
$script:ProblemCount++
|
||||
$fixText = ($Fix -split "`n") -join "`n "
|
||||
$script:Guide += "`n$($script:ProblemCount)) $What`n Why: $Why`n Fix: $fixText`n Next: Tell the agent once this is done - it runs this check again.`n"
|
||||
}
|
||||
|
||||
function Exit-WithGuide {
|
||||
if ($script:ProblemCount -eq 1) {
|
||||
$noun = '1 thing needs'
|
||||
} else {
|
||||
$noun = "$($script:ProblemCount) things need"
|
||||
}
|
||||
Write-Line ''
|
||||
Write-Line "STOP - $noun your attention before this wiki can run."
|
||||
Write-Line '(Agent: show this output to the user exactly as it is, then wait. Do not install'
|
||||
Write-Line 'anything yourself and do not work around it.)'
|
||||
[Console]::Out.Write($script:Guide)
|
||||
exit 42
|
||||
}
|
||||
|
||||
# --- the manifest ---------------------------------------------------------------
|
||||
|
||||
$ManifestLines = @(
|
||||
Get-Content -LiteralPath $Manifest -Encoding utf8 |
|
||||
ForEach-Object { $_.TrimEnd("`r") } |
|
||||
Where-Object { $_.Trim() -ne '' -and -not $_.StartsWith('#') }
|
||||
)
|
||||
|
||||
function Get-ManifestField {
|
||||
param([string]$Kind, [string]$Name, [int]$Field)
|
||||
foreach ($line in $ManifestLines) {
|
||||
$parts = $line.Split('|')
|
||||
if ($parts.Count -gt 1 -and $parts[0] -eq $Kind -and $parts[1] -eq $Name) {
|
||||
if ($Field -le $parts.Count) {
|
||||
return $parts[$Field - 1]
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
$Tools = @()
|
||||
foreach ($line in $ManifestLines) {
|
||||
$parts = $line.Split('|')
|
||||
if ($parts[0] -ne 'tool') {
|
||||
continue
|
||||
}
|
||||
if ($parts[1] -notmatch '^[a-z0-9]+$') {
|
||||
Write-ErrorLine "preflight: bad tool name '$($parts[1])' in $Manifest"
|
||||
exit 1
|
||||
}
|
||||
if ($parts[3] -eq 'all' -or $parts[3] -eq $Platform) {
|
||||
$Tools += $parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
function Get-InstallHint {
|
||||
param([string]$Tool)
|
||||
$choco = Get-ManifestField 'tool' $Tool 7
|
||||
$winget = Get-ManifestField 'tool' $Tool 8
|
||||
$brew = Get-ManifestField 'tool' $Tool 9
|
||||
$apt = Get-ManifestField 'tool' $Tool 10
|
||||
$pacman = Get-ManifestField 'tool' $Tool 11
|
||||
$hint = ''
|
||||
switch ($Platform) {
|
||||
'windows' {
|
||||
if ((Get-Command choco -CommandType Application -ErrorAction SilentlyContinue) -and $choco -ne '-') {
|
||||
$hint = "$choco (in a terminal opened as administrator)"
|
||||
} elseif ((Get-Command winget -CommandType Application -ErrorAction SilentlyContinue) -and $winget -ne '-') {
|
||||
$hint = $winget
|
||||
}
|
||||
}
|
||||
'macos' {
|
||||
if ((Get-Command brew -CommandType Application -ErrorAction SilentlyContinue) -and $brew -ne '-') {
|
||||
$hint = $brew
|
||||
}
|
||||
}
|
||||
default {
|
||||
if ((Get-Command apt-get -CommandType Application -ErrorAction SilentlyContinue) -and $apt -ne '-') {
|
||||
$hint = $apt
|
||||
} elseif ((Get-Command pacman -CommandType Application -ErrorAction SilentlyContinue) -and $pacman -ne '-') {
|
||||
$hint = $pacman
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($hint) {
|
||||
return $hint
|
||||
}
|
||||
$lines = @('Install it with the package manager this computer uses, for example:')
|
||||
foreach ($entry in @($choco, $winget, $brew, $apt, $pacman)) {
|
||||
if ($entry -and $entry -ne '-') {
|
||||
$lines += " $entry"
|
||||
}
|
||||
}
|
||||
return ($lines -join "`n")
|
||||
}
|
||||
|
||||
# --- version helpers ------------------------------------------------------------
|
||||
|
||||
# major.minor of the first version-looking token in the text ("git version 2.47.1" -> 2.47)
|
||||
function Get-VersionOf {
|
||||
param([string]$Text)
|
||||
$first = ($Text -split "`n" | Select-Object -First 1)
|
||||
if ($null -eq $first) {
|
||||
return ''
|
||||
}
|
||||
$match = [regex]::Match($first, '(\d+)(?:\.(\d+))?')
|
||||
if (-not $match.Success) {
|
||||
return ''
|
||||
}
|
||||
if ($match.Groups[2].Success) {
|
||||
return "$($match.Groups[1].Value).$($match.Groups[2].Value)"
|
||||
}
|
||||
return $match.Groups[1].Value
|
||||
}
|
||||
|
||||
function Test-VersionGe {
|
||||
param([string]$Have, [string]$Want)
|
||||
$haveParts = ("$Have.0" -split '\.')[0, 1] | ForEach-Object { [int]$_ }
|
||||
$wantParts = ("$Want.0" -split '\.')[0, 1] | ForEach-Object { [int]$_ }
|
||||
if ($haveParts[0] -ne $wantParts[0]) {
|
||||
return $haveParts[0] -gt $wantParts[0]
|
||||
}
|
||||
return $haveParts[1] -ge $wantParts[1]
|
||||
}
|
||||
|
||||
# --- finding tools --------------------------------------------------------------
|
||||
|
||||
# The Microsoft Store's app-execution aliases: a python.exe that opens the Store
|
||||
# instead of running anything. Never executed, never recorded.
|
||||
function Test-StoreAlias {
|
||||
param([string]$Path)
|
||||
return $Path -match '(?i)[\\/]windowsapps[\\/]'
|
||||
}
|
||||
|
||||
function Test-Usable {
|
||||
param([string]$Path)
|
||||
if (-not $Path -or -not (Test-Path -LiteralPath $Path -PathType Leaf) -or (Test-StoreAlias $Path)) {
|
||||
return $false
|
||||
}
|
||||
if ($IsWindows) {
|
||||
return $true
|
||||
}
|
||||
return (([int][IO.File]::GetUnixFileMode($Path)) -band 73) -ne 0
|
||||
}
|
||||
|
||||
# PATH as this process has it, plus - on Windows - whatever the registry holds that a
|
||||
# long-running session has not picked up yet (a tool installed after it started).
|
||||
function Get-SearchDirectory {
|
||||
$separator = [IO.Path]::PathSeparator
|
||||
$directories = [Collections.Generic.List[string]]::new()
|
||||
$sources = @($env:PATH)
|
||||
if ($IsWindows) {
|
||||
foreach ($scope in 'Machine', 'User') {
|
||||
$sources += [Environment]::GetEnvironmentVariable('Path', $scope)
|
||||
}
|
||||
}
|
||||
foreach ($source in $sources) {
|
||||
if (-not $source) {
|
||||
continue
|
||||
}
|
||||
foreach ($entry in $source.Split($separator)) {
|
||||
$expanded = [Environment]::ExpandEnvironmentVariables($entry.Trim())
|
||||
if ($expanded -and -not $directories.Contains($expanded)) {
|
||||
$directories.Add($expanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
return $directories
|
||||
}
|
||||
|
||||
# Every executable called <name> on PATH, in PATH order, store aliases dropped.
|
||||
function Find-OnPath {
|
||||
param([string]$Name)
|
||||
$found = @()
|
||||
foreach ($directory in (Get-SearchDirectory)) {
|
||||
foreach ($file in @($Name, "$Name.exe")) {
|
||||
$candidate = Join-Path $directory $file
|
||||
if (Test-Usable $candidate) {
|
||||
$found += $candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
return $found
|
||||
}
|
||||
|
||||
# Runs a program; its standard output joined by newlines, or $null when it did not
|
||||
# start or did not exit 0.
|
||||
function Invoke-Native {
|
||||
param([string]$Path, [string[]]$Arguments = @())
|
||||
try {
|
||||
$output = & $Path @Arguments 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
return $null
|
||||
}
|
||||
return (@($output | ForEach-Object { "$_".TrimEnd("`r") }) -join "`n")
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# Same, but with the error stream merged in, for the messages the user is shown.
|
||||
function Invoke-NativeVerbose {
|
||||
param([string]$Path, [string[]]$Arguments = @())
|
||||
try {
|
||||
$output = & $Path @Arguments 2>&1
|
||||
return [pscustomobject]@{
|
||||
Code = $LASTEXITCODE
|
||||
Output = (@($output | ForEach-Object { "$_".TrimEnd("`r") }) -join "`n")
|
||||
}
|
||||
} catch {
|
||||
return [pscustomobject]@{ Code = -1; Output = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
|
||||
# The value recorded for <name> in .wikitool-tools.json.
|
||||
function Get-RecordedPath {
|
||||
param([string]$Name)
|
||||
if (-not (Test-Path -LiteralPath $ToolsFile -PathType Leaf)) {
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
$data = Get-Content -Raw -LiteralPath $ToolsFile | ConvertFrom-Json -AsHashtable
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
if ($data -is [hashtable] -and $data.ContainsKey('tools') -and $data['tools'] -is [hashtable] -and
|
||||
$data['tools'].ContainsKey($Name)) {
|
||||
return [string]$data['tools'][$Name]
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function Get-SetValue {
|
||||
param([string]$Name)
|
||||
if ($Sets.ContainsKey($Name)) {
|
||||
return [string]$Sets[$Name]
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
# --- --set: every named path has to work, or nothing is written -------------------
|
||||
|
||||
foreach ($name in $Sets.Keys) {
|
||||
if ($Tools -notcontains $name) {
|
||||
Write-ErrorLine "preflight: --set names '$name', which is not a tool this platform needs: $($Tools -join ' ')"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- python -------------------------------------------------------------------------
|
||||
|
||||
$script:Py = ''
|
||||
$script:PyVersion = ''
|
||||
$script:PyOld = $null
|
||||
$PyMin = Get-ManifestField 'tool' 'python' 3
|
||||
|
||||
# Sets Py/PyVersion on success, PyOld when the version is too old.
|
||||
function Test-PythonCandidate {
|
||||
param([string]$Path, [string[]]$Extra = @())
|
||||
$out = Invoke-Native -Path $Path -Arguments ($Extra + @('-c', $PyProbe))
|
||||
if ($null -eq $out) {
|
||||
return $false
|
||||
}
|
||||
$lines = $out -split "`n"
|
||||
if ($lines.Count -lt 2) {
|
||||
return $false
|
||||
}
|
||||
$version = $lines[0].Trim()
|
||||
$executable = $lines[1].Trim()
|
||||
if (-not $version -or -not $executable) {
|
||||
return $false
|
||||
}
|
||||
if (Test-VersionGe $version $PyMin) {
|
||||
$script:Py = $executable
|
||||
$script:PyVersion = $version
|
||||
return $true
|
||||
}
|
||||
$script:PyOld = @{ Version = $version; Path = $Path }
|
||||
return $false
|
||||
}
|
||||
|
||||
# py and py.exe are the launcher: they need -3
|
||||
function Get-PythonExtra {
|
||||
param([string]$Path)
|
||||
if ((Split-Path -Leaf $Path) -in 'py', 'py.exe') {
|
||||
return @('-3')
|
||||
}
|
||||
return @()
|
||||
}
|
||||
|
||||
$given = Get-SetValue 'python'
|
||||
if ($given) {
|
||||
if (-not (Test-Usable $given) -or -not (Test-PythonCandidate $given (Get-PythonExtra $given))) {
|
||||
Add-Problem "The path given for Python does not work: $given" `
|
||||
"every wikitool command runs on Python $PyMin or newer, and this path did not start one" `
|
||||
"Check the path - it has to be the python executable itself, version $PyMin or newer.`nThen run: $Self --set python=<full path to python>"
|
||||
$script:BadSet = $true
|
||||
}
|
||||
} else {
|
||||
$previous = Get-RecordedPath 'python'
|
||||
if ($previous -and (Test-Usable $previous)) {
|
||||
[void](Test-PythonCandidate $previous)
|
||||
}
|
||||
if (-not $script:Py) {
|
||||
if ($Platform -eq 'windows') {
|
||||
$order = @(@('python', @()), @('py', @('-3')), @('python3', @()))
|
||||
} else {
|
||||
$order = @(@('python3', @()), @('python', @()))
|
||||
}
|
||||
foreach ($candidate in $order) {
|
||||
foreach ($path in (Find-OnPath $candidate[0])) {
|
||||
if (Test-PythonCandidate $path $candidate[1]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($script:Py) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- the other tools --------------------------------------------------------------
|
||||
|
||||
$Report = @()
|
||||
$Found = @{}
|
||||
|
||||
function Add-Report {
|
||||
param([string]$Status, [string]$Name, [string]$Version, [string]$Path)
|
||||
$script:Report += ('{0,-8} {1,-7} {2,-8} {3}' -f $Status, $Name, $Version, $Path)
|
||||
}
|
||||
|
||||
if ($script:Py) {
|
||||
Add-Report 'OK' 'python' $script:PyVersion $script:Py
|
||||
$Found['python'] = $script:Py
|
||||
} elseif (-not $script:BadSet) {
|
||||
$label = Get-ManifestField 'tool' 'python' 5
|
||||
$why = Get-ManifestField 'tool' 'python' 6
|
||||
if ($script:PyOld) {
|
||||
Add-Report 'TOO_OLD' 'python' $script:PyOld.Version $script:PyOld.Path
|
||||
Add-Problem "$label $($script:PyOld.Version) is too old - this stack needs $PyMin or newer." $why `
|
||||
"$(Get-InstallHint 'python')`nOr, if a newer one is already installed, give its full path:`n$Self --set python=<full path to python>"
|
||||
} else {
|
||||
Add-Report 'MISSING' 'python' '-' '-'
|
||||
Add-Problem "$label $PyMin or newer was not found." $why `
|
||||
"$(Get-InstallHint 'python')`nOr, if it is installed somewhere this check did not look, give its full path:`n$Self --set python=<full path to python>"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tool in $Tools) {
|
||||
if ($tool -eq 'python') {
|
||||
continue
|
||||
}
|
||||
$label = Get-ManifestField 'tool' $tool 5
|
||||
$why = Get-ManifestField 'tool' $tool 6
|
||||
$minimum = Get-ManifestField 'tool' $tool 3
|
||||
$given = Get-SetValue $tool
|
||||
$path = ''
|
||||
if ($given) {
|
||||
if (Test-Usable $given) {
|
||||
$path = $given
|
||||
} else {
|
||||
Add-Problem "The path given for $label does not work: $given" $why `
|
||||
"Check the path - it has to be the $tool executable itself.`nThen run: $Self --set $tool=<full path to $tool>"
|
||||
$script:BadSet = $true
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
$previous = Get-RecordedPath $tool
|
||||
if ($tool -eq 'pwsh' -and (Test-Usable ([Environment]::ProcessPath))) {
|
||||
$path = [Environment]::ProcessPath
|
||||
} elseif ($previous -and (Test-Usable $previous)) {
|
||||
$path = $previous
|
||||
} else {
|
||||
$path = [string](Find-OnPath $tool | Select-Object -First 1)
|
||||
}
|
||||
}
|
||||
if (-not $path) {
|
||||
Add-Report 'MISSING' $tool '-' '-'
|
||||
Add-Problem "$label was not found." $why `
|
||||
"$(Get-InstallHint $tool)`nOr, if it is installed somewhere this check did not look, give its full path:`n$Self --set $tool=<full path to $tool>"
|
||||
continue
|
||||
}
|
||||
$version = Get-VersionOf ([string](Invoke-Native -Path $path -Arguments @('--version')))
|
||||
if ($minimum -ne '-' -and (-not $version -or -not (Test-VersionGe $version $minimum))) {
|
||||
$shown = if ($version) { $version } else { 'unknown' }
|
||||
Add-Report 'TOO_OLD' $tool $shown $path
|
||||
Add-Problem "$label $(if ($version) { $version } else { 'of unknown version' }) is too old - this stack needs $minimum or newer." $why `
|
||||
(Get-InstallHint $tool)
|
||||
continue
|
||||
}
|
||||
Add-Report 'OK' $tool $(if ($version) { $version } else { '-' }) $path
|
||||
$Found[$tool] = $path
|
||||
}
|
||||
|
||||
# --- install folder length (Windows, long paths off) ------------------------------
|
||||
|
||||
function Test-LongPathsEnabled {
|
||||
if ($env:CHEMENU_PREFLIGHT_LONGPATHS) {
|
||||
return $env:CHEMENU_PREFLIGHT_LONGPATHS -eq '1'
|
||||
}
|
||||
# An unreadable key counts as "off": the limit then protects a machine it did not
|
||||
# have to.
|
||||
if (-not $IsWindows) {
|
||||
return $false
|
||||
}
|
||||
try {
|
||||
$value = Get-ItemPropertyValue -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name LongPathsEnabled
|
||||
return $value -eq 1
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($Platform -eq 'windows' -and -not (Test-LongPathsEnabled)) {
|
||||
$limit = [int](Get-ManifestField 'limit' 'install_dir_max' 3)
|
||||
$length = $Root.Length
|
||||
if ($length -gt $limit) {
|
||||
Add-Problem "The folder this wiki is installed in is too long ($length characters, at most $limit): $Root" `
|
||||
"Windows on this computer only allows paths of up to 259 characters, and the wiki's own files need the rest" `
|
||||
"Move the wiki to a shorter folder, for example C:\Chemenu, and run this check there.`nAlternatively, someone with administrator rights can turn on long paths in Windows."
|
||||
}
|
||||
}
|
||||
|
||||
# --- execution policy and Mark of the Web (Windows) -------------------------------
|
||||
|
||||
# The policy that decides whether tools/wikitool.ps1 starts in an ordinary pwsh: the
|
||||
# first scope that sets one, the group policies first. This process's own scope is left
|
||||
# out - it holds the bypass this script was started with.
|
||||
function Get-PolicyEntry {
|
||||
if ($env:CHEMENU_PREFLIGHT_POLICY) {
|
||||
$entries = @()
|
||||
foreach ($pair in $env:CHEMENU_PREFLIGHT_POLICY.Split(',')) {
|
||||
$scope, $policy = $pair.Split('=', 2)
|
||||
$entries += [pscustomobject]@{ Scope = $scope.Trim(); ExecutionPolicy = $policy.Trim() }
|
||||
}
|
||||
return $entries
|
||||
}
|
||||
return @(Get-ExecutionPolicy -List)
|
||||
}
|
||||
|
||||
function Test-ExecutionPolicy {
|
||||
$effective = $null
|
||||
foreach ($scope in 'MachinePolicy', 'UserPolicy', 'CurrentUser', 'LocalMachine') {
|
||||
$entry = Get-PolicyEntry | Where-Object { [string]$_.Scope -eq $scope } | Select-Object -First 1
|
||||
if ($entry -and [string]$entry.ExecutionPolicy -ne 'Undefined') {
|
||||
$effective = @{ Scope = $scope; Policy = [string]$entry.ExecutionPolicy }
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($null -eq $effective -or $effective.Policy -notin 'Restricted', 'AllSigned') {
|
||||
return
|
||||
}
|
||||
$why = 'tools/wikitool.ps1 is not signed, and this policy only lets signed scripts (or none) run'
|
||||
if ($effective.Scope -in 'MachinePolicy', 'UserPolicy') {
|
||||
Add-Problem "A group policy ($($effective.Scope)) sets the PowerShell execution policy to $($effective.Policy)." $why `
|
||||
"A group policy cannot be overridden from this computer. Ask whoever looks after it to allow locally written scripts (RemoteSigned) for PowerShell 7,`nor run the wiki's commands from Git Bash (tools/wikitool instead of tools/wikitool.ps1)."
|
||||
} else {
|
||||
Add-Problem "The PowerShell execution policy is $($effective.Policy) (set for $($effective.Scope))." $why `
|
||||
"In a PowerShell 7 window, run:`nSet-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned"
|
||||
}
|
||||
}
|
||||
|
||||
# Scripts below tools/ that carry a Mark of the Web from the internet zone - a file
|
||||
# saved by a browser or unpacked from such a download in Explorer. Invoke-WebRequest and
|
||||
# tar set none, so this only turns up on the manual path.
|
||||
function Get-MarkedScript {
|
||||
if ($env:CHEMENU_PREFLIGHT_MARKED) {
|
||||
return @($env:CHEMENU_PREFLIGHT_MARKED.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
}
|
||||
if (-not $IsWindows) {
|
||||
return @()
|
||||
}
|
||||
$marked = @()
|
||||
foreach ($file in Get-ChildItem -LiteralPath $Dir -Filter '*.ps1' -Recurse -File) {
|
||||
if ($file.FullName.StartsWith($Venv, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
continue
|
||||
}
|
||||
$zone = Get-Content -LiteralPath $file.FullName -Stream Zone.Identifier -ErrorAction SilentlyContinue
|
||||
if ($zone -match 'ZoneId=([3-9])') {
|
||||
$marked += $file.FullName.Substring($Dir.Length + 1)
|
||||
}
|
||||
}
|
||||
return $marked
|
||||
}
|
||||
|
||||
if ($Platform -eq 'windows') {
|
||||
Test-ExecutionPolicy
|
||||
$marked = @(Get-MarkedScript)
|
||||
if ($marked.Count -gt 0) {
|
||||
$listed = (($marked | Select-Object -First 5) -join ', ') + $(if ($marked.Count -gt 5) { ', ...' } else { '' })
|
||||
Add-Problem "Windows marks some of this wiki's scripts as downloaded from the internet: $listed" `
|
||||
'PowerShell refuses to run a marked script under its usual settings - tools/wikitool.ps1 would not start. This happens when the wiki was downloaded with a browser and unpacked in Explorer' `
|
||||
"In a PowerShell 7 window, run:`nGet-ChildItem -LiteralPath '$Root' -Recurse -File | Unblock-File"
|
||||
}
|
||||
}
|
||||
|
||||
# --- record what was found ----------------------------------------------------------
|
||||
|
||||
function ConvertTo-JsonString {
|
||||
param([string]$Text)
|
||||
return $Text.Replace('\', '\\').Replace('"', '\"')
|
||||
}
|
||||
|
||||
function Write-ToolsFile {
|
||||
param([bool]$Complete)
|
||||
$flag = if ($Complete) { 'true' } else { 'false' }
|
||||
$text = "{`n `"schema`": 1,`n `"complete`": $flag,`n `"tools`": {"
|
||||
$separator = ''
|
||||
foreach ($tool in $Tools) {
|
||||
if ($Found.ContainsKey($tool)) {
|
||||
$text += "$separator`n `"$tool`": `"$(ConvertTo-JsonString $Found[$tool])`""
|
||||
$separator = ','
|
||||
}
|
||||
}
|
||||
$text += "`n }`n}`n"
|
||||
$temporary = "$ToolsFile.tmp.$PID"
|
||||
[IO.File]::WriteAllText($temporary, $text, [Text.UTF8Encoding]::new($false))
|
||||
Move-Item -LiteralPath $temporary -Destination $ToolsFile -Force
|
||||
}
|
||||
|
||||
foreach ($line in $Report) {
|
||||
Write-Line $line
|
||||
}
|
||||
|
||||
if ($script:BadSet) {
|
||||
Exit-WithGuide # nothing is written for a path that does not work
|
||||
}
|
||||
Write-ToolsFile $false
|
||||
if ($script:ProblemCount -gt 0) {
|
||||
Exit-WithGuide
|
||||
}
|
||||
|
||||
# --- tools/.venv ----------------------------------------------------------------------
|
||||
|
||||
function Get-VenvPython {
|
||||
$unix = Join-Path (Join-Path $Venv 'bin') 'python'
|
||||
$windows = Join-Path (Join-Path $Venv 'Scripts') 'python.exe'
|
||||
if ((Test-Path -LiteralPath $unix -PathType Leaf) -and (Test-Usable $unix)) {
|
||||
return $unix
|
||||
}
|
||||
if (Test-Path -LiteralPath $windows -PathType Leaf) {
|
||||
return $windows
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function Get-LastLine {
|
||||
param([string]$Text)
|
||||
return ((($Text -split "`n") | Select-Object -Last 5 | ForEach-Object { " $_" }) -join "`n")
|
||||
}
|
||||
|
||||
$venvPython = Get-VenvPython
|
||||
if (-not $venvPython -or $null -eq (Invoke-Native -Path $venvPython -Arguments @('-m', 'pip', '--version'))) {
|
||||
$created = Invoke-NativeVerbose -Path $script:Py -Arguments @('-m', 'venv', '--clear', $Venv)
|
||||
$venvPython = Get-VenvPython
|
||||
if ($created.Code -ne 0 -or -not $venvPython) {
|
||||
$fix = "Python said:`n$(Get-LastLine $created.Output)"
|
||||
if ($Platform -eq 'linux' -and (Get-Command apt-get -CommandType Application -ErrorAction SilentlyContinue)) {
|
||||
$fix = "sudo apt install python3-venv`n$fix"
|
||||
}
|
||||
Add-Problem 'The wiki''s own Python environment (tools/.venv) could not be created.' `
|
||||
'wikitool runs in that environment, so that nothing it installs touches the rest of the computer' `
|
||||
$fix
|
||||
Exit-WithGuide
|
||||
}
|
||||
}
|
||||
|
||||
$want = (Get-FileHash -LiteralPath $Requirements -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$have = ''
|
||||
if (Test-Path -LiteralPath $Stamp -PathType Leaf) {
|
||||
$have = (Get-Content -Raw -LiteralPath $Stamp).Trim()
|
||||
}
|
||||
if ($want -ne $have) {
|
||||
$installed = Invoke-NativeVerbose -Path $venvPython -Arguments @('-m', 'pip', 'install', '--disable-pip-version-check', '--quiet', '-r', $Requirements)
|
||||
if ($installed.Code -ne 0) {
|
||||
Add-Problem 'The libraries wikitool needs could not be installed into tools/.venv.' `
|
||||
'they are downloaded once from the internet; without them no wikitool command starts' `
|
||||
"Check that this computer can reach the internet (a proxy or a security program can block it; if so, ask whoever looks after this computer).`npip said:`n$(Get-LastLine $installed.Output)"
|
||||
Exit-WithGuide
|
||||
}
|
||||
[IO.File]::WriteAllText($Stamp, "$want`n", [Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
Write-Line ('OK venv - {0}' -f $Venv)
|
||||
|
||||
Write-ToolsFile $true
|
||||
Write-Line ''
|
||||
Write-Line 'Preflight passed. Tool paths are recorded in .wikitool-tools.json; tools/wikitool is ready.'
|
||||
exit 0
|
||||
Reference in new issue
Block a user