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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,13 @@ jobs:
sudo apt-get update
sudo apt-get install -y shellcheck bats

- name: Verify phpvm.sh matches linux/src (drift check)
run: bash ./build.sh --check

- name: ShellCheck
# Deliberately only the generated file: linux/src/*.sh are fragments,
# not standalone scripts. The drift check above guarantees the generated
# file is what those modules say.
run: shellcheck linux/install.sh linux/phpvm.sh

- name: Bats (offline)
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -395,22 +395,28 @@ 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:
`windows/phpvm.ps1` and `linux/phpvm.sh` are **generated** — do not edit them
directly. The sources live in `windows/src/*.ps1` and `linux/src/*.sh`, one file
per domain, and are concatenated back into the single shipped script per OS:

```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.
```bash
bash ./build.sh # rebuild linux/phpvm.sh from linux/src/
bash ./build.sh --check # fail if the two have drifted (what CI gates on)
```

`linux/phpvm.sh` is hand-written and not part of the build.
The numeric filename prefixes set the concat order. On Windows it is
load-bearing throughout: `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. On Linux only the
ends matter — bash resolves function bodies at call time, so `00-header.sh`
(constants) must come first and `99-entry.sh` (source-time init) last; the
modules in between are ordered for readability. Distribution is unaffected — the
installer and `phpvm upgrade` still fetch one file per OS.

Tests:

Expand Down
79 changes: 79 additions & 0 deletions build.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# ==============================================================================
# Concatenate linux/src/*.sh into the shipped linux/phpvm.sh.
#
# phpvm is distributed as a single file: linux/install.sh downloads
# linux/phpvm.sh 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. In
# bash only two things about that order are load-bearing: 00-header.sh defines
# the constants everything else reads, and 99-entry.sh ends with the
# source-time init block, which calls functions that must already be defined.
# Function definition order in between is free — pick it for readability.
#
# Usage:
# bash ./build.sh # write linux/phpvm.sh
# bash ./build.sh --check # compare only; non-zero exit on drift (CI gate)
# ==============================================================================
set -euo pipefail

root="$(cd "$(dirname "$0")" && pwd)"
src_dir="$root/linux/src"
out_file="$root/linux/phpvm.sh"

check=0
case "${1:-}" in
--check) check=1 ;;
"") ;;
*) echo "usage: build.sh [--check]" >&2; exit 2 ;;
esac

# LC_ALL=C so the glob sorts the same everywhere; the repo stores LF only.
modules=$(LC_ALL=C ls "$src_dir"/*.sh 2>/dev/null || true)
[[ -n "$modules" ]] || { echo "No modules found in $src_dir" >&2; exit 1; }

tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

{
cat <<'BANNER'
#!/usr/bin/env bash
# ==============================================================================
# GENERATED FILE - DO NOT EDIT
# Built from linux/src/*.sh (concatenated in filename order).
# Edit a module there, then run: bash ./build.sh
# CI fails the drift check if this file and the modules disagree.
# ==============================================================================
BANNER

count=0
while IFS= read -r m; do
name=$(basename "$m")
dashes=$(printf '%*s' $(( 74 - ${#name} > 1 ? 74 - ${#name} : 1 )) '' | tr ' ' '-')
printf '\n# --- src/%s %s\n' "$name" "$dashes"
# $(cat) drops trailing newlines - each module contributes exactly one
# trailing blank line, no matter how it was saved.
printf '%s\n' "$(cat "$m")"
count=$(( count + 1 ))
done <<< "$modules"
} > "$tmp"

module_count=$(printf '%s\n' "$modules" | wc -l | tr -d ' ')

if (( check )); then
[[ -f "$out_file" ]] || { echo "Missing $out_file - run: bash ./build.sh" >&2; exit 1; }
if diff -u "$out_file" "$tmp" > /dev/null; then
echo "OK: linux/phpvm.sh matches linux/src/*.sh ($module_count modules)."
exit 0
fi
echo "DRIFT: linux/phpvm.sh does not match linux/src/*.sh."
echo "Rebuild with: bash ./build.sh"
diff -u "$out_file" "$tmp" | head -40
exit 1
fi

cp "$tmp" "$out_file"
chmod +x "$out_file"
echo "Built linux/phpvm.sh from $module_count modules ($(wc -l < "$out_file" | tr -d ' ') lines)."
2 changes: 1 addition & 1 deletion linux/install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@

set -e

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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,13 @@ jobs:
sudo apt-get update
sudo apt-get install -y shellcheck bats

- name: Verify phpvm.sh matches linux/src (drift check)
run: bash ./build.sh --check

- name: ShellCheck
# Deliberately only the generated file: linux/src/*.sh are fragments,
# not standalone scripts. The drift check above guarantees the generated
# file is what those modules say.
run: shellcheck linux/install.sh linux/phpvm.sh

- name: Bats (offline)
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -395,22 +395,28 @@ 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:
`windows/phpvm.ps1` and `linux/phpvm.sh` are **generated** — do not edit them
directly. The sources live in `windows/src/*.ps1` and `linux/src/*.sh`, one file
per domain, and are concatenated back into the single shipped script per OS:

```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.
```bash
bash ./build.sh # rebuild linux/phpvm.sh from linux/src/
bash ./build.sh --check # fail if the two have drifted (what CI gates on)
```

`linux/phpvm.sh` is hand-written and not part of the build.
The numeric filename prefixes set the concat order. On Windows it is
load-bearing throughout: `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. On Linux only the
ends matter — bash resolves function bodies at call time, so `00-header.sh`
(constants) must come first and `99-entry.sh` (source-time init) last; the
modules in between are ordered for readability. Distribution is unaffected — the
installer and `phpvm upgrade` still fetch one file per OS.

Tests:

Expand Down
79 changes: 79 additions & 0 deletions build.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# ==============================================================================
# Concatenate linux/src/*.sh into the shipped linux/phpvm.sh.
#
# phpvm is distributed as a single file: linux/install.sh downloads
# linux/phpvm.sh 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. In
# bash only two things about that order are load-bearing: 00-header.sh defines
# the constants everything else reads, and 99-entry.sh ends with the
# source-time init block, which calls functions that must already be defined.
# Function definition order in between is free — pick it for readability.
#
# Usage:
# bash ./build.sh # write linux/phpvm.sh
# bash ./build.sh --check # compare only; non-zero exit on drift (CI gate)
# ==============================================================================
set -euo pipefail

root="$(cd "$(dirname "$0")" && pwd)"
src_dir="$root/linux/src"
out_file="$root/linux/phpvm.sh"

check=0
case "${1:-}" in
--check) check=1 ;;
"") ;;
*) echo "usage: build.sh [--check]" >&2; exit 2 ;;
esac

# LC_ALL=C so the glob sorts the same everywhere; the repo stores LF only.
modules=$(LC_ALL=C ls "$src_dir"/*.sh 2>/dev/null || true)
[[ -n "$modules" ]] || { echo "No modules found in $src_dir" >&2; exit 1; }

tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

{
cat <<'BANNER'
#!/usr/bin/env bash
# ==============================================================================
# GENERATED FILE - DO NOT EDIT
# Built from linux/src/*.sh (concatenated in filename order).
# Edit a module there, then run: bash ./build.sh
# CI fails the drift check if this file and the modules disagree.
# ==============================================================================
BANNER

count=0
while IFS= read -r m; do
name=$(basename "$m")
dashes=$(printf '%*s' $(( 74 - ${#name} > 1 ? 74 - ${#name} : 1 )) '' | tr ' ' '-')
printf '\n# --- src/%s %s\n' "$name" "$dashes"
# $(cat) drops trailing newlines - each module contributes exactly one
# trailing blank line, no matter how it was saved.
printf '%s\n' "$(cat "$m")"
count=$(( count + 1 ))
done <<< "$modules"
} > "$tmp"

module_count=$(printf '%s\n' "$modules" | wc -l | tr -d ' ')

if (( check )); then
[[ -f "$out_file" ]] || { echo "Missing $out_file - run: bash ./build.sh" >&2; exit 1; }
if diff -u "$out_file" "$tmp" > /dev/null; then
echo "OK: linux/phpvm.sh matches linux/src/*.sh ($module_count modules)."
exit 0
fi
echo "DRIFT: linux/phpvm.sh does not match linux/src/*.sh."
echo "Rebuild with: bash ./build.sh"
diff -u "$out_file" "$tmp" | head -40
exit 1
fi

cp "$tmp" "$out_file"
chmod +x "$out_file"
echo "Built linux/phpvm.sh from $module_count modules ($(wc -l < "$out_file" | tr -d ' ') lines)."
2 changes: 1 addition & 1 deletion linux/install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@

set -e

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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,13 @@ jobs:
sudo apt-get update
sudo apt-get install -y shellcheck bats

- name: Verify phpvm.sh matches linux/src (drift check)
run: bash ./build.sh --check

- name: ShellCheck
# Deliberately only the generated file: linux/src/*.sh are fragments,
# not standalone scripts. The drift check above guarantees the generated
# file is what those modules say.
run: shellcheck linux/install.sh linux/phpvm.sh

- name: Bats (offline)
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -395,22 +395,28 @@ 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:
`windows/phpvm.ps1` and `linux/phpvm.sh` are **generated** — do not edit them
directly. The sources live in `windows/src/*.ps1` and `linux/src/*.sh`, one file
per domain, and are concatenated back into the single shipped script per OS:

```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.
```bash
bash ./build.sh # rebuild linux/phpvm.sh from linux/src/
bash ./build.sh --check # fail if the two have drifted (what CI gates on)
```

`linux/phpvm.sh` is hand-written and not part of the build.
The numeric filename prefixes set the concat order. On Windows it is
load-bearing throughout: `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. On Linux only the
ends matter — bash resolves function bodies at call time, so `00-header.sh`
(constants) must come first and `99-entry.sh` (source-time init) last; the
modules in between are ordered for readability. Distribution is unaffected — the
installer and `phpvm upgrade` still fetch one file per OS.

Tests:

Expand Down
79 changes: 79 additions & 0 deletions build.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# ==============================================================================
# Concatenate linux/src/*.sh into the shipped linux/phpvm.sh.
#
# phpvm is distributed as a single file: linux/install.sh downloads
# linux/phpvm.sh 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. In
# bash only two things about that order are load-bearing: 00-header.sh defines
# the constants everything else reads, and 99-entry.sh ends with the
# source-time init block, which calls functions that must already be defined.
# Function definition order in between is free — pick it for readability.
#
# Usage:
# bash ./build.sh # write linux/phpvm.sh
# bash ./build.sh --check # compare only; non-zero exit on drift (CI gate)
# ==============================================================================
set -euo pipefail

root="$(cd "$(dirname "$0")" && pwd)"
src_dir="$root/linux/src"
out_file="$root/linux/phpvm.sh"

check=0
case "${1:-}" in
--check) check=1 ;;
"") ;;
*) echo "usage: build.sh [--check]" >&2; exit 2 ;;
esac

# LC_ALL=C so the glob sorts the same everywhere; the repo stores LF only.
modules=$(LC_ALL=C ls "$src_dir"/*.sh 2>/dev/null || true)
[[ -n "$modules" ]] || { echo "No modules found in $src_dir" >&2; exit 1; }

tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

{
cat <<'BANNER'
#!/usr/bin/env bash
# ==============================================================================
# GENERATED FILE - DO NOT EDIT
# Built from linux/src/*.sh (concatenated in filename order).
# Edit a module there, then run: bash ./build.sh
# CI fails the drift check if this file and the modules disagree.
# ==============================================================================
BANNER

count=0
while IFS= read -r m; do
name=$(basename "$m")
dashes=$(printf '%*s' $(( 74 - ${#name} > 1 ? 74 - ${#name} : 1 )) '' | tr ' ' '-')
printf '\n# --- src/%s %s\n' "$name" "$dashes"
# $(cat) drops trailing newlines - each module contributes exactly one
# trailing blank line, no matter how it was saved.
printf '%s\n' "$(cat "$m")"
count=$(( count + 1 ))
done <<< "$modules"
} > "$tmp"

module_count=$(printf '%s\n' "$modules" | wc -l | tr -d ' ')

if (( check )); then
[[ -f "$out_file" ]] || { echo "Missing $out_file - run: bash ./build.sh" >&2; exit 1; }
if diff -u "$out_file" "$tmp" > /dev/null; then
echo "OK: linux/phpvm.sh matches linux/src/*.sh ($module_count modules)."
exit 0
fi
echo "DRIFT: linux/phpvm.sh does not match linux/src/*.sh."
echo "Rebuild with: bash ./build.sh"
diff -u "$out_file" "$tmp" | head -40
exit 1
fi

cp "$tmp" "$out_file"
chmod +x "$out_file"
echo "Built linux/phpvm.sh from $module_count modules ($(wc -l < "$out_file" | tr -d ' ') lines)."
2 changes: 1 addition & 1 deletion linux/install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@

set -e

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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,13 @@ jobs:
sudo apt-get update
sudo apt-get install -y shellcheck bats

- name: Verify phpvm.sh matches linux/src (drift check)
run: bash ./build.sh --check

- name: ShellCheck
# Deliberately only the generated file: linux/src/*.sh are fragments,
# not standalone scripts. The drift check above guarantees the generated
# file is what those modules say.
run: shellcheck linux/install.sh linux/phpvm.sh

- name: Bats (offline)
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -395,22 +395,28 @@ 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:
`windows/phpvm.ps1` and `linux/phpvm.sh` are **generated** — do not edit them
directly. The sources live in `windows/src/*.ps1` and `linux/src/*.sh`, one file
per domain, and are concatenated back into the single shipped script per OS:

```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.
```bash
bash ./build.sh # rebuild linux/phpvm.sh from linux/src/
bash ./build.sh --check # fail if the two have drifted (what CI gates on)
```

`linux/phpvm.sh` is hand-written and not part of the build.
The numeric filename prefixes set the concat order. On Windows it is
load-bearing throughout: `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. On Linux only the
ends matter — bash resolves function bodies at call time, so `00-header.sh`
(constants) must come first and `99-entry.sh` (source-time init) last; the
modules in between are ordered for readability. Distribution is unaffected — the
installer and `phpvm upgrade` still fetch one file per OS.

Tests:

Expand Down
79 changes: 79 additions & 0 deletions build.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# ==============================================================================
# Concatenate linux/src/*.sh into the shipped linux/phpvm.sh.
#
# phpvm is distributed as a single file: linux/install.sh downloads
# linux/phpvm.sh 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. In
# bash only two things about that order are load-bearing: 00-header.sh defines
# the constants everything else reads, and 99-entry.sh ends with the
# source-time init block, which calls functions that must already be defined.
# Function definition order in between is free — pick it for readability.
#
# Usage:
# bash ./build.sh # write linux/phpvm.sh
# bash ./build.sh --check # compare only; non-zero exit on drift (CI gate)
# ==============================================================================
set -euo pipefail

root="$(cd "$(dirname "$0")" && pwd)"
src_dir="$root/linux/src"
out_file="$root/linux/phpvm.sh"

check=0
case "${1:-}" in
--check) check=1 ;;
"") ;;
*) echo "usage: build.sh [--check]" >&2; exit 2 ;;
esac

# LC_ALL=C so the glob sorts the same everywhere; the repo stores LF only.
modules=$(LC_ALL=C ls "$src_dir"/*.sh 2>/dev/null || true)
[[ -n "$modules" ]] || { echo "No modules found in $src_dir" >&2; exit 1; }

tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

{
cat <<'BANNER'
#!/usr/bin/env bash
# ==============================================================================
# GENERATED FILE - DO NOT EDIT
# Built from linux/src/*.sh (concatenated in filename order).
# Edit a module there, then run: bash ./build.sh
# CI fails the drift check if this file and the modules disagree.
# ==============================================================================
BANNER

count=0
while IFS= read -r m; do
name=$(basename "$m")
dashes=$(printf '%*s' $(( 74 - ${#name} > 1 ? 74 - ${#name} : 1 )) '' | tr ' ' '-')
printf '\n# --- src/%s %s\n' "$name" "$dashes"
# $(cat) drops trailing newlines - each module contributes exactly one
# trailing blank line, no matter how it was saved.
printf '%s\n' "$(cat "$m")"
count=$(( count + 1 ))
done <<< "$modules"
} > "$tmp"

module_count=$(printf '%s\n' "$modules" | wc -l | tr -d ' ')

if (( check )); then
[[ -f "$out_file" ]] || { echo "Missing $out_file - run: bash ./build.sh" >&2; exit 1; }
if diff -u "$out_file" "$tmp" > /dev/null; then
echo "OK: linux/phpvm.sh matches linux/src/*.sh ($module_count modules)."
exit 0
fi
echo "DRIFT: linux/phpvm.sh does not match linux/src/*.sh."
echo "Rebuild with: bash ./build.sh"
diff -u "$out_file" "$tmp" | head -40
exit 1
fi

cp "$tmp" "$out_file"
chmod +x "$out_file"
echo "Built linux/phpvm.sh from $module_count modules ($(wc -l < "$out_file" | tr -d ' ') lines)."
2 changes: 1 addition & 1 deletion linux/install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@

set -e

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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,13 @@ jobs:
sudo apt-get update
sudo apt-get install -y shellcheck bats

- name: Verify phpvm.sh matches linux/src (drift check)
run: bash ./build.sh --check

- name: ShellCheck
# Deliberately only the generated file: linux/src/*.sh are fragments,
# not standalone scripts. The drift check above guarantees the generated
# file is what those modules say.
run: shellcheck linux/install.sh linux/phpvm.sh

- name: Bats (offline)
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -395,22 +395,28 @@ 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:
`windows/phpvm.ps1` and `linux/phpvm.sh` are **generated** — do not edit them
directly. The sources live in `windows/src/*.ps1` and `linux/src/*.sh`, one file
per domain, and are concatenated back into the single shipped script per OS:

```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.
```bash
bash ./build.sh # rebuild linux/phpvm.sh from linux/src/
bash ./build.sh --check # fail if the two have drifted (what CI gates on)
```

`linux/phpvm.sh` is hand-written and not part of the build.
The numeric filename prefixes set the concat order. On Windows it is
load-bearing throughout: `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. On Linux only the
ends matter — bash resolves function bodies at call time, so `00-header.sh`
(constants) must come first and `99-entry.sh` (source-time init) last; the
modules in between are ordered for readability. Distribution is unaffected — the
installer and `phpvm upgrade` still fetch one file per OS.

Tests:

Expand Down
79 changes: 79 additions & 0 deletions build.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# ==============================================================================
# Concatenate linux/src/*.sh into the shipped linux/phpvm.sh.
#
# phpvm is distributed as a single file: linux/install.sh downloads
# linux/phpvm.sh 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. In
# bash only two things about that order are load-bearing: 00-header.sh defines
# the constants everything else reads, and 99-entry.sh ends with the
# source-time init block, which calls functions that must already be defined.
# Function definition order in between is free — pick it for readability.
#
# Usage:
# bash ./build.sh # write linux/phpvm.sh
# bash ./build.sh --check # compare only; non-zero exit on drift (CI gate)
# ==============================================================================
set -euo pipefail

root="$(cd "$(dirname "$0")" && pwd)"
src_dir="$root/linux/src"
out_file="$root/linux/phpvm.sh"

check=0
case "${1:-}" in
--check) check=1 ;;
"") ;;
*) echo "usage: build.sh [--check]" >&2; exit 2 ;;
esac

# LC_ALL=C so the glob sorts the same everywhere; the repo stores LF only.
modules=$(LC_ALL=C ls "$src_dir"/*.sh 2>/dev/null || true)
[[ -n "$modules" ]] || { echo "No modules found in $src_dir" >&2; exit 1; }

tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

{
cat <<'BANNER'
#!/usr/bin/env bash
# ==============================================================================
# GENERATED FILE - DO NOT EDIT
# Built from linux/src/*.sh (concatenated in filename order).
# Edit a module there, then run: bash ./build.sh
# CI fails the drift check if this file and the modules disagree.
# ==============================================================================
BANNER

count=0
while IFS= read -r m; do
name=$(basename "$m")
dashes=$(printf '%*s' $(( 74 - ${#name} > 1 ? 74 - ${#name} : 1 )) '' | tr ' ' '-')
printf '\n# --- src/%s %s\n' "$name" "$dashes"
# $(cat) drops trailing newlines - each module contributes exactly one
# trailing blank line, no matter how it was saved.
printf '%s\n' "$(cat "$m")"
count=$(( count + 1 ))
done <<< "$modules"
} > "$tmp"

module_count=$(printf '%s\n' "$modules" | wc -l | tr -d ' ')

if (( check )); then
[[ -f "$out_file" ]] || { echo "Missing $out_file - run: bash ./build.sh" >&2; exit 1; }
if diff -u "$out_file" "$tmp" > /dev/null; then
echo "OK: linux/phpvm.sh matches linux/src/*.sh ($module_count modules)."
exit 0
fi
echo "DRIFT: linux/phpvm.sh does not match linux/src/*.sh."
echo "Rebuild with: bash ./build.sh"
diff -u "$out_file" "$tmp" | head -40
exit 1
fi

cp "$tmp" "$out_file"
chmod +x "$out_file"
echo "Built linux/phpvm.sh from $module_count modules ($(wc -l < "$out_file" | tr -d ' ') lines)."
2 changes: 1 addition & 1 deletion linux/install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@

set -e

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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,13 @@ jobs:
sudo apt-get update
sudo apt-get install -y shellcheck bats

- name: Verify phpvm.sh matches linux/src (drift check)
run: bash ./build.sh --check

- name: ShellCheck
# Deliberately only the generated file: linux/src/*.sh are fragments,
# not standalone scripts. The drift check above guarantees the generated
# file is what those modules say.
run: shellcheck linux/install.sh linux/phpvm.sh

- name: Bats (offline)
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -395,22 +395,28 @@ 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:
`windows/phpvm.ps1` and `linux/phpvm.sh` are **generated** — do not edit them
directly. The sources live in `windows/src/*.ps1` and `linux/src/*.sh`, one file
per domain, and are concatenated back into the single shipped script per OS:

```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.
```bash
bash ./build.sh # rebuild linux/phpvm.sh from linux/src/
bash ./build.sh --check # fail if the two have drifted (what CI gates on)
```

`linux/phpvm.sh` is hand-written and not part of the build.
The numeric filename prefixes set the concat order. On Windows it is
load-bearing throughout: `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. On Linux only the
ends matter — bash resolves function bodies at call time, so `00-header.sh`
(constants) must come first and `99-entry.sh` (source-time init) last; the
modules in between are ordered for readability. Distribution is unaffected — the
installer and `phpvm upgrade` still fetch one file per OS.

Tests:

Expand Down
79 changes: 79 additions & 0 deletions build.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# ==============================================================================
# Concatenate linux/src/*.sh into the shipped linux/phpvm.sh.
#
# phpvm is distributed as a single file: linux/install.sh downloads
# linux/phpvm.sh 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. In
# bash only two things about that order are load-bearing: 00-header.sh defines
# the constants everything else reads, and 99-entry.sh ends with the
# source-time init block, which calls functions that must already be defined.
# Function definition order in between is free — pick it for readability.
#
# Usage:
# bash ./build.sh # write linux/phpvm.sh
# bash ./build.sh --check # compare only; non-zero exit on drift (CI gate)
# ==============================================================================
set -euo pipefail

root="$(cd "$(dirname "$0")" && pwd)"
src_dir="$root/linux/src"
out_file="$root/linux/phpvm.sh"

check=0
case "${1:-}" in
--check) check=1 ;;
"") ;;
*) echo "usage: build.sh [--check]" >&2; exit 2 ;;
esac

# LC_ALL=C so the glob sorts the same everywhere; the repo stores LF only.
modules=$(LC_ALL=C ls "$src_dir"/*.sh 2>/dev/null || true)
[[ -n "$modules" ]] || { echo "No modules found in $src_dir" >&2; exit 1; }

tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

{
cat <<'BANNER'
#!/usr/bin/env bash
# ==============================================================================
# GENERATED FILE - DO NOT EDIT
# Built from linux/src/*.sh (concatenated in filename order).
# Edit a module there, then run: bash ./build.sh
# CI fails the drift check if this file and the modules disagree.
# ==============================================================================
BANNER

count=0
while IFS= read -r m; do
name=$(basename "$m")
dashes=$(printf '%*s' $(( 74 - ${#name} > 1 ? 74 - ${#name} : 1 )) '' | tr ' ' '-')
printf '\n# --- src/%s %s\n' "$name" "$dashes"
# $(cat) drops trailing newlines - each module contributes exactly one
# trailing blank line, no matter how it was saved.
printf '%s\n' "$(cat "$m")"
count=$(( count + 1 ))
done <<< "$modules"
} > "$tmp"

module_count=$(printf '%s\n' "$modules" | wc -l | tr -d ' ')

if (( check )); then
[[ -f "$out_file" ]] || { echo "Missing $out_file - run: bash ./build.sh" >&2; exit 1; }
if diff -u "$out_file" "$tmp" > /dev/null; then
echo "OK: linux/phpvm.sh matches linux/src/*.sh ($module_count modules)."
exit 0
fi
echo "DRIFT: linux/phpvm.sh does not match linux/src/*.sh."
echo "Rebuild with: bash ./build.sh"
diff -u "$out_file" "$tmp" | head -40
exit 1
fi

cp "$tmp" "$out_file"
chmod +x "$out_file"
echo "Built linux/phpvm.sh from $module_count modules ($(wc -l < "$out_file" | tr -d ' ') lines)."
2 changes: 1 addition & 1 deletion linux/install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@

set -e

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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,13 @@ jobs:
sudo apt-get update
sudo apt-get install -y shellcheck bats

- name: Verify phpvm.sh matches linux/src (drift check)
run: bash ./build.sh --check

- name: ShellCheck
# Deliberately only the generated file: linux/src/*.sh are fragments,
# not standalone scripts. The drift check above guarantees the generated
# file is what those modules say.
run: shellcheck linux/install.sh linux/phpvm.sh

- name: Bats (offline)
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -395,22 +395,28 @@ 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:
`windows/phpvm.ps1` and `linux/phpvm.sh` are **generated** — do not edit them
directly. The sources live in `windows/src/*.ps1` and `linux/src/*.sh`, one file
per domain, and are concatenated back into the single shipped script per OS:

```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.
```bash
bash ./build.sh # rebuild linux/phpvm.sh from linux/src/
bash ./build.sh --check # fail if the two have drifted (what CI gates on)
```

`linux/phpvm.sh` is hand-written and not part of the build.
The numeric filename prefixes set the concat order. On Windows it is
load-bearing throughout: `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. On Linux only the
ends matter — bash resolves function bodies at call time, so `00-header.sh`
(constants) must come first and `99-entry.sh` (source-time init) last; the
modules in between are ordered for readability. Distribution is unaffected — the
installer and `phpvm upgrade` still fetch one file per OS.

Tests:

Expand Down
79 changes: 79 additions & 0 deletions build.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# ==============================================================================
# Concatenate linux/src/*.sh into the shipped linux/phpvm.sh.
#
# phpvm is distributed as a single file: linux/install.sh downloads
# linux/phpvm.sh 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. In
# bash only two things about that order are load-bearing: 00-header.sh defines
# the constants everything else reads, and 99-entry.sh ends with the
# source-time init block, which calls functions that must already be defined.
# Function definition order in between is free — pick it for readability.
#
# Usage:
# bash ./build.sh # write linux/phpvm.sh
# bash ./build.sh --check # compare only; non-zero exit on drift (CI gate)
# ==============================================================================
set -euo pipefail

root="$(cd "$(dirname "$0")" && pwd)"
src_dir="$root/linux/src"
out_file="$root/linux/phpvm.sh"

check=0
case "${1:-}" in
--check) check=1 ;;
"") ;;
*) echo "usage: build.sh [--check]" >&2; exit 2 ;;
esac

# LC_ALL=C so the glob sorts the same everywhere; the repo stores LF only.
modules=$(LC_ALL=C ls "$src_dir"/*.sh 2>/dev/null || true)
[[ -n "$modules" ]] || { echo "No modules found in $src_dir" >&2; exit 1; }

tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

{
cat <<'BANNER'
#!/usr/bin/env bash
# ==============================================================================
# GENERATED FILE - DO NOT EDIT
# Built from linux/src/*.sh (concatenated in filename order).
# Edit a module there, then run: bash ./build.sh
# CI fails the drift check if this file and the modules disagree.
# ==============================================================================
BANNER

count=0
while IFS= read -r m; do
name=$(basename "$m")
dashes=$(printf '%*s' $(( 74 - ${#name} > 1 ? 74 - ${#name} : 1 )) '' | tr ' ' '-')
printf '\n# --- src/%s %s\n' "$name" "$dashes"
# $(cat) drops trailing newlines - each module contributes exactly one
# trailing blank line, no matter how it was saved.
printf '%s\n' "$(cat "$m")"
count=$(( count + 1 ))
done <<< "$modules"
} > "$tmp"

module_count=$(printf '%s\n' "$modules" | wc -l | tr -d ' ')

if (( check )); then
[[ -f "$out_file" ]] || { echo "Missing $out_file - run: bash ./build.sh" >&2; exit 1; }
if diff -u "$out_file" "$tmp" > /dev/null; then
echo "OK: linux/phpvm.sh matches linux/src/*.sh ($module_count modules)."
exit 0
fi
echo "DRIFT: linux/phpvm.sh does not match linux/src/*.sh."
echo "Rebuild with: bash ./build.sh"
diff -u "$out_file" "$tmp" | head -40
exit 1
fi

cp "$tmp" "$out_file"
chmod +x "$out_file"
echo "Built linux/phpvm.sh from $module_count modules ($(wc -l < "$out_file" | tr -d ' ') lines)."
2 changes: 1 addition & 1 deletion linux/install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@

set -e

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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,13 @@ jobs:
sudo apt-get update
sudo apt-get install -y shellcheck bats

- name: Verify phpvm.sh matches linux/src (drift check)
run: bash ./build.sh --check

- name: ShellCheck
# Deliberately only the generated file: linux/src/*.sh are fragments,
# not standalone scripts. The drift check above guarantees the generated
# file is what those modules say.
run: shellcheck linux/install.sh linux/phpvm.sh

- name: Bats (offline)
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -395,22 +395,28 @@ 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:
`windows/phpvm.ps1` and `linux/phpvm.sh` are **generated** — do not edit them
directly. The sources live in `windows/src/*.ps1` and `linux/src/*.sh`, one file
per domain, and are concatenated back into the single shipped script per OS:

```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.
```bash
bash ./build.sh # rebuild linux/phpvm.sh from linux/src/
bash ./build.sh --check # fail if the two have drifted (what CI gates on)
```

`linux/phpvm.sh` is hand-written and not part of the build.
The numeric filename prefixes set the concat order. On Windows it is
load-bearing throughout: `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. On Linux only the
ends matter — bash resolves function bodies at call time, so `00-header.sh`
(constants) must come first and `99-entry.sh` (source-time init) last; the
modules in between are ordered for readability. Distribution is unaffected — the
installer and `phpvm upgrade` still fetch one file per OS.

Tests:

Expand Down
79 changes: 79 additions & 0 deletions build.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# ==============================================================================
# Concatenate linux/src/*.sh into the shipped linux/phpvm.sh.
#
# phpvm is distributed as a single file: linux/install.sh downloads
# linux/phpvm.sh 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. In
# bash only two things about that order are load-bearing: 00-header.sh defines
# the constants everything else reads, and 99-entry.sh ends with the
# source-time init block, which calls functions that must already be defined.
# Function definition order in between is free — pick it for readability.
#
# Usage:
# bash ./build.sh # write linux/phpvm.sh
# bash ./build.sh --check # compare only; non-zero exit on drift (CI gate)
# ==============================================================================
set -euo pipefail

root="$(cd "$(dirname "$0")" && pwd)"
src_dir="$root/linux/src"
out_file="$root/linux/phpvm.sh"

check=0
case "${1:-}" in
--check) check=1 ;;
"") ;;
*) echo "usage: build.sh [--check]" >&2; exit 2 ;;
esac

# LC_ALL=C so the glob sorts the same everywhere; the repo stores LF only.
modules=$(LC_ALL=C ls "$src_dir"/*.sh 2>/dev/null || true)
[[ -n "$modules" ]] || { echo "No modules found in $src_dir" >&2; exit 1; }

tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

{
cat <<'BANNER'
#!/usr/bin/env bash
# ==============================================================================
# GENERATED FILE - DO NOT EDIT
# Built from linux/src/*.sh (concatenated in filename order).
# Edit a module there, then run: bash ./build.sh
# CI fails the drift check if this file and the modules disagree.
# ==============================================================================
BANNER

count=0
while IFS= read -r m; do
name=$(basename "$m")
dashes=$(printf '%*s' $(( 74 - ${#name} > 1 ? 74 - ${#name} : 1 )) '' | tr ' ' '-')
printf '\n# --- src/%s %s\n' "$name" "$dashes"
# $(cat) drops trailing newlines - each module contributes exactly one
# trailing blank line, no matter how it was saved.
printf '%s\n' "$(cat "$m")"
count=$(( count + 1 ))
done <<< "$modules"
} > "$tmp"

module_count=$(printf '%s\n' "$modules" | wc -l | tr -d ' ')

if (( check )); then
[[ -f "$out_file" ]] || { echo "Missing $out_file - run: bash ./build.sh" >&2; exit 1; }
if diff -u "$out_file" "$tmp" > /dev/null; then
echo "OK: linux/phpvm.sh matches linux/src/*.sh ($module_count modules)."
exit 0
fi
echo "DRIFT: linux/phpvm.sh does not match linux/src/*.sh."
echo "Rebuild with: bash ./build.sh"
diff -u "$out_file" "$tmp" | head -40
exit 1
fi

cp "$tmp" "$out_file"
chmod +x "$out_file"
echo "Built linux/phpvm.sh from $module_count modules ($(wc -l < "$out_file" | tr -d ' ') lines)."
2 changes: 1 addition & 1 deletion linux/install.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@

set -e

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

Expand Down
Loading
Loading