From a423d840e31eedb8853efae0becd25bf15123933 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Wed, 22 Jul 2026 14:37:09 +1000 Subject: [PATCH] punk-runtime: deterministic ordinal listing order; AGENTS .ps1-twin knowledge corrected The user-observed ordering discrepancy decomposed into two edition divergences, both now handled: - launch paths run DIFFERENT powershells: the .ps1 twin executes under the invoking shell (pwsh 7) while the .cmd routes via the wrap-pinned 'cmd.exe /c powershell' (Windows PowerShell 5); - between those editions BOTH Hashtable key enumeration order AND culture-sensitive Sort-Object collation differ (NLS vs ICU treat hyphens differently). Fix: ordinal comparison for every name ordering (candidates, platforms dirs, list -remote local+remote-only rows) in ps1, LC_ALL=C glob/ls/sort in bash - verified byte-identical list -remote output across cmd->powershell5, pwsh7-on-twin, and bash. bin/AGENTS.md .ps1-twin paragraph corrected (was stale): the twin is SELF-MATERIALIZED by the polyglot's batch layer - created when missing, fc-compared and re-copied when content differs - so it self-heals after a re-wrap on the next .cmd launch; no manual copy step (verified: deleted twin regenerated byte-identical). Documented caveats: a direct-.ps1-only user can ride a stale twin until a .cmd launch heals it, and payload code must avoid edition-specific behaviour. Rewrapped; roundtrip pin PASS; layout copy refreshed (twin deliberately NOT hand-copied - the mechanism owns it now). Project 0.18.7. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 4 ++ bin/AGENTS.md | 18 +++++- bin/punk-runtime.cmd | 55 +++++++++++++------ punkproject.toml | 2 +- .../punk/project-0.1/bin/punk-runtime.cmd | 55 +++++++++++++------ src/scriptapps/bin/punk-runtime.bash | 22 ++++---- src/scriptapps/bin/punk-runtime.ps1 | 33 +++++++++-- 7 files changed, 137 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cec461c2..b3bd68b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` "Project Versioning" section for the bump policy. +## [0.18.7] - 2026-07-22 + +- `bin/punk-runtime.cmd`: deterministic listing order everywhere. Investigation of an ordering discrepancy between launch paths found two edition divergences: the `.ps1` twin runs under the INVOKING powershell (pwsh 7) while the `.cmd` routes via wrap-pinned Windows PowerShell 5, and both Hashtable enumeration order AND culture-sensitive `Sort-Object` collation (NLS vs ICU) differ between those editions. All name orderings now use ordinal comparison (ps1) / `LC_ALL=C` (bash), which agree byte-for-byte across pwsh, powershell and bash. Also corrected `bin/AGENTS.md`: the `.ps1` twin is SELF-MATERIALIZED by the polyglot's batch layer (created when missing, `fc`-compared and re-copied when stale) - the previous "refresh both on re-wrap" manual-copy guidance was stale; twin verified regenerating byte-identical after deletion. + ## [0.18.6] - 2026-07-22 - `bin/punk-runtime.cmd` `list -remote`: surfaces the selection state - summary lines show the platform's server default (per `defaults.txt`, best-effort fetch) and the locally active runtime (annotated `(= server default)` on match); the active runtime's row carries the local listing's `*` marker and the default's row (local or remote-only) is annotated `(server default)`. Also fixed a latent bash-payload bug the work exposed: CRLF server sha1sums made locally-present runtimes additionally appear as remote-only rows (`\r`-suffixed name failed the existence test; msys grep strips `\r`, bash `read` does not) - punkbin's maintenance script now writes LF sha1sums too. diff --git a/bin/AGENTS.md b/bin/AGENTS.md index 30594461..4117cba6 100644 --- a/bin/AGENTS.md +++ b/bin/AGENTS.md @@ -55,9 +55,21 @@ external tool under its own name (`dtplite`, `sdx`, `kettle`), and `getpunk` (already punkshell-specific; intended future single-download cross-platform entry point). Policy decided by the user 2026-07-20 (G-096); the remaining pre-policy names were swept under G-097 (2026-07-21): `punk-bits`, `punk-runtime`, `punk-tclargs` and the -punk- prefixed selfsign experiment scripts. Where launchability convenience warrants it a `.ps1` twin of the generated -`.cmd` ships alongside (windows users may start from cmd.exe or powershell); the twin -is a byte-copy under the other extension - refresh both on re-wrap. +punk- prefixed selfsign experiment scripts. + +`.ps1` twin (corrected 2026-07-22 - the twin is SELF-MATERIALIZED, not manually +refreshed): the generated `.cmd`'s batch layer creates `.ps1` beside itself +when missing AND `fc`-compares/re-copies it whenever content differs (powershell +needs a `.ps1` extension for `-File`), so the twin regenerates on any `.cmd` launch +and self-heals after a re-wrap - no manual copy step. The twin is a byte-copy of the +polyglot and is VCS-ignored (a runtime artifact, unlike the committed `.cmd`). +Caveats: a user who only ever launches the `.ps1` directly can ride a stale twin +until the next `.cmd`-path launch heals it; and the two launch paths run DIFFERENT +powershells - `./bin/` in a powershell session resolves the `.ps1` and runs it +under the INVOKING shell (pwsh 7 or powershell 5), while the `.cmd` route is pinned +by the wrap toml (`cmd.exe /c powershell` = Windows PowerShell) - payload code must +be edition-portable and avoid implementation-defined behaviour (e.g. Hashtable +enumeration order differs between the editions; sort explicitly). ### Launch package modes (built punk shells) diff --git a/bin/punk-runtime.cmd b/bin/punk-runtime.cmd index ca22937d..229b4c6f 100755 --- a/bin/punk-runtime.cmd +++ b/bin/punk-runtime.cmd @@ -1453,9 +1453,11 @@ set_active() { echo "active runtime for $archtail set to: $1 (recorded in $active_file)" } #installed runtime candidates - exclude directories, build copies and -#non-runtime files (ps1-payload parity: files only, same extension excludes) +#non-runtime files (ps1-payload parity: files only, same extension excludes). +#LC_ALL=C: glob expansion order is collation-dependent - C-locale byte order +#matches the ps1 payload's ordinal sorting, so listings agree everywhere. list_candidates() { - local f + local f LC_ALL=C if [[ -d "$archdir" ]]; then for f in "$archdir"/*; do [[ -f "$f" ]] || continue @@ -1768,19 +1770,17 @@ case "$action" in [[ -n "$platform_default" && "$f" == "$platform_default" ]] && annot=" (server default)" printf '%s%-35s %s%s\n' "$mark" "$f" "$rhs" "$annot" done - #remote-only entries - while IFS= read -r line; do - #server sha1sums may be CRLF: msys grep strips \r in text mode but - #bash read does not - without this a locally-present runtime also - #shows as a remote-only row (its \r-suffixed name fails the -f test) - line="${line%$'\r'}" - rname=$(printf '%s' "$line" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p') + #remote-only entries, sorted (ps1-payload parity - its dict enumeration + #is explicitly sorted). tr strips CRLF \r: msys grep strips it in text + #mode but bash read does not - without this a locally-present runtime + #also shows as a remote-only row (\r-suffixed name fails the -f test) + tr -d '\r' < "$sha1local" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p' | LC_ALL=C sort | while IFS= read -r rname; do if [[ -n "$rname" && ! -f "$archdir/$rname" ]]; then annot="" [[ -n "$platform_default" && "$rname" == "$platform_default" ]] && annot=" (server default)" printf ' %-35s %s%s\n' "-" "$rname" "$annot" fi - done < "$sha1local" + done echo "-----------------------------------------------------------------------" echo "* = active (local selection)" else @@ -1921,7 +1921,7 @@ case "$action" in rtroot="${scriptdir}/runtime" localdirs="" if [[ -d "$rtroot" ]]; then - localdirs=$(ls -1 "$rtroot" 2>/dev/null | while read -r d; do [[ -d "$rtroot/$d" ]] && echo "$d"; done) + localdirs=$(LC_ALL=C ls -1 "$rtroot" 2>/dev/null | while read -r d; do [[ -d "$rtroot/$d" ]] && echo "$d"; done) fi if [[ "${1:-}" == "-remote" ]]; then manifesturl="${url_kitbase}/platforms.txt" @@ -2359,13 +2359,27 @@ function Set-PunkActiveRuntime { [System.IO.File]::WriteAllText($activefile, "active = `"$name`"`n", [System.Text.UTF8Encoding]::new($false)) Write-Host "active runtime set to: $name (recorded in $activefile)" } -#installed runtime candidates - exclude build copies and non-runtime files +#installed runtime candidates - exclude build copies and non-runtime files. +#ORDINAL name order: culture-sensitive Sort-Object collates differently between +#windows powershell (NLS) and pwsh (ICU) - both launch paths exist (the .ps1 +#twin runs under the invoking shell) - and ordinal matches the bash payload's +#LC_ALL=C ordering, so listings agree byte-for-byte everywhere. function Get-PunkRuntimeCandidates { param([string] $archfolder) if (-not (Test-Path -Path $archfolder -PathType Container)) { return @() } - return @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not ($(".txt",".toml",".tm",".tmp",".log") -contains $_.Extension) } | Sort-Object Name) + $files = @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not ($(".txt",".toml",".tm",".tmp",".log") -contains $_.Extension) }) + if ($files.Count -le 1) { + return $files + } + $byname = @{} + foreach ($f in $files) { $byname[$f.Name] = $f } + $names = @($byname.Keys) + [array]::Sort($names, [System.StringComparer]::Ordinal) + $out = @() + foreach ($n in $names) { $out += $byname[$n] } + return $out } #operator help (the 'help' action; the no-args case shows the Usage block only). #Keep in sync with the bash payload's show_help/show_usage. @@ -3063,7 +3077,13 @@ function psmain { $Y = "`e[33m" #Yellow $R = "`e[31m" #Red $RST = "`e[m" - foreach ($key in $localdict.Keys) { + #explicit ORDINAL sort: Hashtable key enumeration order is + #undefined (and differs between the powershell editions), and + #culture-sensitive Sort-Object collates differently too (NLS vs + #ICU) - ordinal agrees across editions and with bash LC_ALL=C + $localkeys = @($localdict.Keys) + [array]::Sort($localkeys, [System.StringComparer]::Ordinal) + foreach ($key in $localkeys) { $local_sha1 = $($localdict[$key]) if ($remotedict.ContainsKey($key)) { if ($local_sha1 -eq $remotedict[$key]) { @@ -3089,7 +3109,9 @@ function psmain { write-host "$rhs$annot" } $lhs_missing = "-".PadRight(35, ' ') - foreach ($key in $remotedict.Keys) { + $remotekeys = @($remotedict.Keys) + [array]::Sort($remotekeys, [System.StringComparer]::Ordinal) + foreach ($key in $remotekeys) { if (-not ($localdict.ContainsKey($key))) { $annot = "" if ($platform_default -ne "" -and $key -eq $platform_default) { $annot = " (server default)" } @@ -3137,7 +3159,8 @@ function psmain { #punkbin layout contract; third-party mirrors carry the same file) $localdirs = @() if (Test-Path -Path $rtfolder -PathType Container) { - $localdirs = @(Get-ChildItem -Path $rtfolder -Directory | Select-Object -ExpandProperty Name | Sort-Object) + $localdirs = @(Get-ChildItem -Path $rtfolder -Directory | Select-Object -ExpandProperty Name) + [array]::Sort($localdirs, [System.StringComparer]::Ordinal) } if ( $PSBoundParameters.ContainsKey('remote') ) { $manifesturl = "$artifacturl/platforms.txt" diff --git a/punkproject.toml b/punkproject.toml index 35458830..493a81dc 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.18.6" +version = "0.18.7" license = "BSD-2-Clause" diff --git a/src/project_layouts/vendor/punk/project-0.1/bin/punk-runtime.cmd b/src/project_layouts/vendor/punk/project-0.1/bin/punk-runtime.cmd index ca22937d..229b4c6f 100644 --- a/src/project_layouts/vendor/punk/project-0.1/bin/punk-runtime.cmd +++ b/src/project_layouts/vendor/punk/project-0.1/bin/punk-runtime.cmd @@ -1453,9 +1453,11 @@ set_active() { echo "active runtime for $archtail set to: $1 (recorded in $active_file)" } #installed runtime candidates - exclude directories, build copies and -#non-runtime files (ps1-payload parity: files only, same extension excludes) +#non-runtime files (ps1-payload parity: files only, same extension excludes). +#LC_ALL=C: glob expansion order is collation-dependent - C-locale byte order +#matches the ps1 payload's ordinal sorting, so listings agree everywhere. list_candidates() { - local f + local f LC_ALL=C if [[ -d "$archdir" ]]; then for f in "$archdir"/*; do [[ -f "$f" ]] || continue @@ -1768,19 +1770,17 @@ case "$action" in [[ -n "$platform_default" && "$f" == "$platform_default" ]] && annot=" (server default)" printf '%s%-35s %s%s\n' "$mark" "$f" "$rhs" "$annot" done - #remote-only entries - while IFS= read -r line; do - #server sha1sums may be CRLF: msys grep strips \r in text mode but - #bash read does not - without this a locally-present runtime also - #shows as a remote-only row (its \r-suffixed name fails the -f test) - line="${line%$'\r'}" - rname=$(printf '%s' "$line" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p') + #remote-only entries, sorted (ps1-payload parity - its dict enumeration + #is explicitly sorted). tr strips CRLF \r: msys grep strips it in text + #mode but bash read does not - without this a locally-present runtime + #also shows as a remote-only row (\r-suffixed name fails the -f test) + tr -d '\r' < "$sha1local" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p' | LC_ALL=C sort | while IFS= read -r rname; do if [[ -n "$rname" && ! -f "$archdir/$rname" ]]; then annot="" [[ -n "$platform_default" && "$rname" == "$platform_default" ]] && annot=" (server default)" printf ' %-35s %s%s\n' "-" "$rname" "$annot" fi - done < "$sha1local" + done echo "-----------------------------------------------------------------------" echo "* = active (local selection)" else @@ -1921,7 +1921,7 @@ case "$action" in rtroot="${scriptdir}/runtime" localdirs="" if [[ -d "$rtroot" ]]; then - localdirs=$(ls -1 "$rtroot" 2>/dev/null | while read -r d; do [[ -d "$rtroot/$d" ]] && echo "$d"; done) + localdirs=$(LC_ALL=C ls -1 "$rtroot" 2>/dev/null | while read -r d; do [[ -d "$rtroot/$d" ]] && echo "$d"; done) fi if [[ "${1:-}" == "-remote" ]]; then manifesturl="${url_kitbase}/platforms.txt" @@ -2359,13 +2359,27 @@ function Set-PunkActiveRuntime { [System.IO.File]::WriteAllText($activefile, "active = `"$name`"`n", [System.Text.UTF8Encoding]::new($false)) Write-Host "active runtime set to: $name (recorded in $activefile)" } -#installed runtime candidates - exclude build copies and non-runtime files +#installed runtime candidates - exclude build copies and non-runtime files. +#ORDINAL name order: culture-sensitive Sort-Object collates differently between +#windows powershell (NLS) and pwsh (ICU) - both launch paths exist (the .ps1 +#twin runs under the invoking shell) - and ordinal matches the bash payload's +#LC_ALL=C ordering, so listings agree byte-for-byte everywhere. function Get-PunkRuntimeCandidates { param([string] $archfolder) if (-not (Test-Path -Path $archfolder -PathType Container)) { return @() } - return @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not ($(".txt",".toml",".tm",".tmp",".log") -contains $_.Extension) } | Sort-Object Name) + $files = @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not ($(".txt",".toml",".tm",".tmp",".log") -contains $_.Extension) }) + if ($files.Count -le 1) { + return $files + } + $byname = @{} + foreach ($f in $files) { $byname[$f.Name] = $f } + $names = @($byname.Keys) + [array]::Sort($names, [System.StringComparer]::Ordinal) + $out = @() + foreach ($n in $names) { $out += $byname[$n] } + return $out } #operator help (the 'help' action; the no-args case shows the Usage block only). #Keep in sync with the bash payload's show_help/show_usage. @@ -3063,7 +3077,13 @@ function psmain { $Y = "`e[33m" #Yellow $R = "`e[31m" #Red $RST = "`e[m" - foreach ($key in $localdict.Keys) { + #explicit ORDINAL sort: Hashtable key enumeration order is + #undefined (and differs between the powershell editions), and + #culture-sensitive Sort-Object collates differently too (NLS vs + #ICU) - ordinal agrees across editions and with bash LC_ALL=C + $localkeys = @($localdict.Keys) + [array]::Sort($localkeys, [System.StringComparer]::Ordinal) + foreach ($key in $localkeys) { $local_sha1 = $($localdict[$key]) if ($remotedict.ContainsKey($key)) { if ($local_sha1 -eq $remotedict[$key]) { @@ -3089,7 +3109,9 @@ function psmain { write-host "$rhs$annot" } $lhs_missing = "-".PadRight(35, ' ') - foreach ($key in $remotedict.Keys) { + $remotekeys = @($remotedict.Keys) + [array]::Sort($remotekeys, [System.StringComparer]::Ordinal) + foreach ($key in $remotekeys) { if (-not ($localdict.ContainsKey($key))) { $annot = "" if ($platform_default -ne "" -and $key -eq $platform_default) { $annot = " (server default)" } @@ -3137,7 +3159,8 @@ function psmain { #punkbin layout contract; third-party mirrors carry the same file) $localdirs = @() if (Test-Path -Path $rtfolder -PathType Container) { - $localdirs = @(Get-ChildItem -Path $rtfolder -Directory | Select-Object -ExpandProperty Name | Sort-Object) + $localdirs = @(Get-ChildItem -Path $rtfolder -Directory | Select-Object -ExpandProperty Name) + [array]::Sort($localdirs, [System.StringComparer]::Ordinal) } if ( $PSBoundParameters.ContainsKey('remote') ) { $manifesturl = "$artifacturl/platforms.txt" diff --git a/src/scriptapps/bin/punk-runtime.bash b/src/scriptapps/bin/punk-runtime.bash index 78751fb5..2fcf54bd 100644 --- a/src/scriptapps/bin/punk-runtime.bash +++ b/src/scriptapps/bin/punk-runtime.bash @@ -173,9 +173,11 @@ set_active() { echo "active runtime for $archtail set to: $1 (recorded in $active_file)" } #installed runtime candidates - exclude directories, build copies and -#non-runtime files (ps1-payload parity: files only, same extension excludes) +#non-runtime files (ps1-payload parity: files only, same extension excludes). +#LC_ALL=C: glob expansion order is collation-dependent - C-locale byte order +#matches the ps1 payload's ordinal sorting, so listings agree everywhere. list_candidates() { - local f + local f LC_ALL=C if [[ -d "$archdir" ]]; then for f in "$archdir"/*; do [[ -f "$f" ]] || continue @@ -488,19 +490,17 @@ case "$action" in [[ -n "$platform_default" && "$f" == "$platform_default" ]] && annot=" (server default)" printf '%s%-35s %s%s\n' "$mark" "$f" "$rhs" "$annot" done - #remote-only entries - while IFS= read -r line; do - #server sha1sums may be CRLF: msys grep strips \r in text mode but - #bash read does not - without this a locally-present runtime also - #shows as a remote-only row (its \r-suffixed name fails the -f test) - line="${line%$'\r'}" - rname=$(printf '%s' "$line" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p') + #remote-only entries, sorted (ps1-payload parity - its dict enumeration + #is explicitly sorted). tr strips CRLF \r: msys grep strips it in text + #mode but bash read does not - without this a locally-present runtime + #also shows as a remote-only row (\r-suffixed name fails the -f test) + tr -d '\r' < "$sha1local" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p' | LC_ALL=C sort | while IFS= read -r rname; do if [[ -n "$rname" && ! -f "$archdir/$rname" ]]; then annot="" [[ -n "$platform_default" && "$rname" == "$platform_default" ]] && annot=" (server default)" printf ' %-35s %s%s\n' "-" "$rname" "$annot" fi - done < "$sha1local" + done echo "-----------------------------------------------------------------------" echo "* = active (local selection)" else @@ -641,7 +641,7 @@ case "$action" in rtroot="${scriptdir}/runtime" localdirs="" if [[ -d "$rtroot" ]]; then - localdirs=$(ls -1 "$rtroot" 2>/dev/null | while read -r d; do [[ -d "$rtroot/$d" ]] && echo "$d"; done) + localdirs=$(LC_ALL=C ls -1 "$rtroot" 2>/dev/null | while read -r d; do [[ -d "$rtroot/$d" ]] && echo "$d"; done) fi if [[ "${1:-}" == "-remote" ]]; then manifesturl="${url_kitbase}/platforms.txt" diff --git a/src/scriptapps/bin/punk-runtime.ps1 b/src/scriptapps/bin/punk-runtime.ps1 index 732c3c1c..57a583ab 100644 --- a/src/scriptapps/bin/punk-runtime.ps1 +++ b/src/scriptapps/bin/punk-runtime.ps1 @@ -62,13 +62,27 @@ function Set-PunkActiveRuntime { [System.IO.File]::WriteAllText($activefile, "active = `"$name`"`n", [System.Text.UTF8Encoding]::new($false)) Write-Host "active runtime set to: $name (recorded in $activefile)" } -#installed runtime candidates - exclude build copies and non-runtime files +#installed runtime candidates - exclude build copies and non-runtime files. +#ORDINAL name order: culture-sensitive Sort-Object collates differently between +#windows powershell (NLS) and pwsh (ICU) - both launch paths exist (the .ps1 +#twin runs under the invoking shell) - and ordinal matches the bash payload's +#LC_ALL=C ordering, so listings agree byte-for-byte everywhere. function Get-PunkRuntimeCandidates { param([string] $archfolder) if (-not (Test-Path -Path $archfolder -PathType Container)) { return @() } - return @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not ($(".txt",".toml",".tm",".tmp",".log") -contains $_.Extension) } | Sort-Object Name) + $files = @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not ($(".txt",".toml",".tm",".tmp",".log") -contains $_.Extension) }) + if ($files.Count -le 1) { + return $files + } + $byname = @{} + foreach ($f in $files) { $byname[$f.Name] = $f } + $names = @($byname.Keys) + [array]::Sort($names, [System.StringComparer]::Ordinal) + $out = @() + foreach ($n in $names) { $out += $byname[$n] } + return $out } #operator help (the 'help' action; the no-args case shows the Usage block only). #Keep in sync with the bash payload's show_help/show_usage. @@ -766,7 +780,13 @@ function psmain { $Y = "`e[33m" #Yellow $R = "`e[31m" #Red $RST = "`e[m" - foreach ($key in $localdict.Keys) { + #explicit ORDINAL sort: Hashtable key enumeration order is + #undefined (and differs between the powershell editions), and + #culture-sensitive Sort-Object collates differently too (NLS vs + #ICU) - ordinal agrees across editions and with bash LC_ALL=C + $localkeys = @($localdict.Keys) + [array]::Sort($localkeys, [System.StringComparer]::Ordinal) + foreach ($key in $localkeys) { $local_sha1 = $($localdict[$key]) if ($remotedict.ContainsKey($key)) { if ($local_sha1 -eq $remotedict[$key]) { @@ -792,7 +812,9 @@ function psmain { write-host "$rhs$annot" } $lhs_missing = "-".PadRight(35, ' ') - foreach ($key in $remotedict.Keys) { + $remotekeys = @($remotedict.Keys) + [array]::Sort($remotekeys, [System.StringComparer]::Ordinal) + foreach ($key in $remotekeys) { if (-not ($localdict.ContainsKey($key))) { $annot = "" if ($platform_default -ne "" -and $key -eq $platform_default) { $annot = " (server default)" } @@ -840,7 +862,8 @@ function psmain { #punkbin layout contract; third-party mirrors carry the same file) $localdirs = @() if (Test-Path -Path $rtfolder -PathType Container) { - $localdirs = @(Get-ChildItem -Path $rtfolder -Directory | Select-Object -ExpandProperty Name | Sort-Object) + $localdirs = @(Get-ChildItem -Path $rtfolder -Directory | Select-Object -ExpandProperty Name) + [array]::Sort($localdirs, [System.StringComparer]::Ordinal) } if ( $PSBoundParameters.ContainsKey('remote') ) { $manifesturl = "$artifacturl/platforms.txt"