Skip to content
Merged
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
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,21 @@ jobs:
Install-Module -Name Pester -MinimumVersion 5.5.0 -Scope CurrentUser -Force -SkipPublisherCheck
Install-Module -Name PSScriptAnalyzer -Scope CurrentUser -Force

- name: Verify phpvm.ps1 matches windows/src (drift check)
shell: pwsh
run: ./build.ps1 -Check

- name: PSScriptAnalyzer
shell: pwsh
run: |
$results = Invoke-ScriptAnalyzer -Path ./windows -Recurse -Settings ./PSScriptAnalyzerSettings.psd1
# Deliberately not -Recurse: windows/src/*.ps1 are fragments, not
# standalone scripts, so linting them alone flags every constant as
# unused and every param() as unreferenced. The drift check above
# guarantees the generated file is what those modules say.
$files = @(Get-ChildItem ./windows -Filter *.ps1) + @(Get-Item ./build.ps1)
$results = $files | ForEach-Object {
Invoke-ScriptAnalyzer -Path $_.FullName -Settings ./PSScriptAnalyzerSettings.psd1
}
if ($results) {
$results | Format-Table -AutoSize
throw "PSScriptAnalyzer found $($results.Count) issue(s)."
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,37 @@ TS/NTS + toolchain and downloads matching extension DLLs.

---

## Development

`windows/phpvm.ps1` is **generated** — do not edit it directly. The Windows
sources live in `windows/src/*.ps1`, one file per domain, and are concatenated
back into the single shipped script:

```powershell
pwsh ./build.ps1 # rebuild windows/phpvm.ps1 from windows/src/
pwsh ./build.ps1 -Check # fail if the two have drifted (what CI gates on)
```

The numeric filename prefixes set the concat order and are load-bearing:
`00-header.ps1` opens with `param()`, which PowerShell requires to be the first
statement, and `99-entry.ps1` closes with the command dispatch, which has to see
every function already defined. Distribution is unaffected — the installer and
`phpvm upgrade` still fetch one file.

`linux/phpvm.sh` is hand-written and not part of the build.

Tests:

```powershell
Invoke-Pester -Configuration (New-PesterConfiguration -Hashtable (Import-PowerShellDataFile ./tests/PesterConfiguration.psd1))
```

```bash
bats tests/linux/
```

---

## License

MIT
69 changes: 69 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<#
.SYNOPSIS
Concatenate windows/src/*.ps1 into the shipped windows/phpvm.ps1.

.DESCRIPTION
phpvm is distributed as a single file: the installer downloads
windows/phpvm.ps1 straight from the repo, and `phpvm upgrade` replaces it
in place. Splitting the sources therefore has to collapse back into one
file at build time rather than at load time.

Modules concatenate in filename order, which is why they are numbered.
The order is load-bearing: 00-header.ps1 opens with param(), which must be
the first statement in the script, and 99-entry.ps1 closes with the command
dispatch, which must see every function already defined.

.PARAMETER Check
Compare instead of write. Exits non-zero when windows/phpvm.ps1 has drifted
from the modules - this is what CI gates on.

.EXAMPLE
pwsh ./build.ps1
pwsh ./build.ps1 -Check
#>
[CmdletBinding()]
param([switch]$Check)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$root = $PSScriptRoot
$srcDir = Join-Path $root 'windows\src'
$outFile = Join-Path $root 'windows\phpvm.ps1'

$banner = @'
# ==============================================================================
# GENERATED FILE - DO NOT EDIT
# Built from windows/src/*.ps1 (concatenated in filename order).
# Edit a module there, then run: pwsh ./build.ps1
# CI fails the drift check if this file and the modules disagree.
# ==============================================================================
'@

$modules = Get-ChildItem $srcDir -Filter '*.ps1' | Sort-Object Name
if (-not $modules) { throw "No modules found in $srcDir" }

# LF throughout: the repo stores LF and CI greps would trip over stray CRs.
$parts = foreach ($m in $modules) {
$text = [System.IO.File]::ReadAllText($m.FullName) -replace "`r`n", "`n"
"# --- src/$($m.Name) " + ('-' * [Math]::Max(1, 74 - $m.Name.Length)) + "`n" + $text.TrimEnd() + "`n"
}
$built = $banner.Replace("`r`n", "`n") + "`n`n" + ($parts -join "`n")

if ($Check) {
if (-not (Test-Path $outFile)) { throw "Missing $outFile - run ./build.ps1" }
$current = [System.IO.File]::ReadAllText($outFile) -replace "`r`n", "`n"
if ($current -ceq $built) {
Write-Host "OK: windows/phpvm.ps1 matches windows/src/*.ps1 ($($modules.Count) modules)."
exit 0
}

Write-Host "DRIFT: windows/phpvm.ps1 does not match windows/src/*.ps1." -ForegroundColor Red
Write-Host "Rebuild with: pwsh ./build.ps1" -ForegroundColor Yellow
$diff = Compare-Object ($current -split "`n") ($built -split "`n")
$diff | Select-Object -First 20 | Format-Table -AutoSize | Out-String | Write-Host
exit 1
}

[System.IO.File]::WriteAllText($outFile, $built, (New-Object System.Text.UTF8Encoding $false))
Write-Host "Built windows/phpvm.ps1 from $($modules.Count) modules ($(($built -split "`n").Count) lines)."
2 changes: 1 addition & 1 deletion linux/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

set -e

PHPVM_VERSION="1.12.3"
PHPVM_VERSION="1.13.0"
PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}"
PHPVM_REPO="https://raw.githubusercontent.com/devhardiyanto/phpvm/main"

Expand Down
12 changes: 11 additions & 1 deletion linux/phpvm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# phpvm use 8.3.0
# ==============================================================================

PHPVM_VERSION="1.12.3"
PHPVM_VERSION="1.13.0"
PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}"
PHPVM_VERSIONS="$PHPVM_DIR/versions"
PHPVM_CURRENT="$PHPVM_DIR/current"
Expand Down Expand Up @@ -493,6 +493,15 @@ phpvm_install() {
gmp_opt="--with-gmp=$(brew --prefix gmp)"
fi

# libiconv is keg-only on Homebrew and ships no pkg-config file, so the
# PKG_CONFIG_PATH prepend below can't find it — configure needs the explicit
# prefix or iconv support fails to detect. Linux resolves iconv from glibc,
# so leave it bare.
local iconv_opt="--with-iconv"
if [[ "$(uname -s)" == "Darwin" ]] && command -v brew &>/dev/null; then
iconv_opt="--with-iconv=$(brew --prefix libiconv)"
fi

local configure_opts=(
"--prefix=$target"
"--with-config-file-path=$target/etc"
Expand Down Expand Up @@ -523,6 +532,7 @@ phpvm_install() {
"--with-freetype"
"$gettext_opt"
"$gmp_opt"
"$iconv_opt"
"--with-pgsql"
"--with-pdo-pgsql"
"--with-onig"
Expand Down
54 changes: 54 additions & 0 deletions tests/windows/Build.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
Describe 'build.ps1' {
BeforeAll {
$script:RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$script:BuildPs1 = Join-Path $script:RepoRoot 'build.ps1'
$script:SrcDir = Join-Path $script:RepoRoot 'windows\src'
$script:Generated = Join-Path $script:RepoRoot 'windows\phpvm.ps1'
}

It 'Ships every module the generated file was built from' {
(Get-ChildItem $script:SrcDir -Filter '*.ps1').Count | Should -BeGreaterThan 0
}

It 'Keeps windows/phpvm.ps1 in sync with windows/src (drift check)' {
# Same gate CI runs. Failing here means: pwsh ./build.ps1
$out = & $script:BuildPs1 -Check 2>&1 | Out-String

$LASTEXITCODE | Should -Be 0 -Because "phpvm.ps1 has drifted:`n$out"
}

It 'Marks the generated file as generated' {
$head = (Get-Content $script:Generated -TotalCount 10) -join "`n"

$head | Should -Match 'GENERATED FILE - DO NOT EDIT'
$head | Should -Match 'build\.ps1'
}

It 'Puts param() before any executable statement' {
# PowerShell rejects the script outright otherwise, so the concat order
# has to keep 00-header.ps1 first.
$lines = Get-Content $script:Generated
$firstCode = ($lines | Where-Object { $_.Trim() -and $_.TrimStart() -notlike '#*' } | Select-Object -First 1)

$firstCode.TrimStart() | Should -BeLike 'param(*'
}

It 'Keeps the command dispatch last' {
$lines = Get-Content $script:Generated
$dispatch = ($lines | Select-String -SimpleMatch 'if (-not $env:PHPVM_NO_ENTRY)').LineNumber
$lastFunc = ($lines | Select-String -Pattern '^function ' | Select-Object -Last 1).LineNumber

$dispatch | Should -BeGreaterThan $lastFunc
}

It 'Declares the version exactly once' {
($lines = Get-Content $script:Generated | Select-String -Pattern '^\$PHPVM_VERSION').Count |
Should -Be 1
}

It 'Carries no CR - the repo stores LF' {
$raw = [System.IO.File]::ReadAllText($script:Generated)
# A dev checkout may be CRLF; only fail on a lone CR the build introduced.
($raw -replace "`r`n", '') | Should -Not -Match "`r"
}
}
Loading
Loading