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
58 changes: 53 additions & 5 deletions apps/demo/scripts/test-updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
from pathlib import Path
import pty
import re
import select
import signal
import subprocess
Expand Down Expand Up @@ -234,18 +235,18 @@ def installs():
self.assertEqual(json.loads(result.stdout)["revision"], "second")
self.assertEqual(installs(), 3)

def test_system_tool_older_than_the_pin_fails_before_any_build(self):
def test_system_tool_below_the_floor_fails_before_any_build(self):
self.stub_compilers()
# Each entry is a different probe shape: `bun --version`, `go version`,
# and Perl's own $^V. Bun 1.3 is the real case: it cannot read this
# repository's lockfile and would otherwise report frozen-lockfile drift.
for language, tool, old, pinned in (("typescript", "BUN", "1.3.14", "bun 1.4.0"),
("go", "GO", "1.20.0", "go 1.26.0"),
("perl", "PERL", "5.40.0.0", "perl 5.44.0.0")):
for language, tool, old, need in (("typescript", "BUN", "1.3.14", "bun 1.4.0"),
("go", "GO", "1.20.0", "go 1.22"),
("perl", "PERL", "5.18.0.0", "perl 5.20")):
result = self.run_demo("--system", language, "--snapshot",
env={**self.env, f"UPDATER_{tool}_VERSION": old})
self.assertNotEqual(result.returncode, 0)
self.assertIn(f"pins {pinned}", result.stderr)
self.assertIn(f"needs {need} or newer", result.stderr)
self.assertIn(f"is {old.rsplit('.', 1)[0] if tool == 'PERL' else old}", result.stderr)
self.assertIn("--mise", result.stderr)
self.assertEqual(result.stdout, "")
Expand All @@ -260,6 +261,53 @@ def test_system_tool_older_than_the_pin_fails_before_any_build(self):
env={**self.env, "UPDATER_BUN_VERSION": "1.3.14"})
self.assertEqual(pinned.returncode, 0, pinned.stderr)

def test_system_tool_between_the_floor_and_the_pin_builds_with_a_note(self):
# The regression this guards: treating the mise pin as a requirement
# refused every stock Mac, where Perl is 5.34 and Ruby 3.3 against pins
# of 5.44 and 4.0. Those build the bindings perfectly well.
self.stub_compilers()
for language, tool, have, pin in (("perl", "PERL", "5.34.0.0", "perl 5.44.0.0"),
("ruby", "RUBY", "3.3.8", "ruby 4.0.6"),
("go", "GO", "1.23.0", "go 1.26.0")):
result = self.run_demo("--system", language, "--snapshot",
env={**self.env, f"UPDATER_{tool}_VERSION": have})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn(f"pins {pin}", result.stderr)
self.assertIn("Rerun with --mise if the build fails", result.stderr)

def test_declared_floors_match_the_ports_own_manifests(self):
"""The floors in demo.sh are copies. This is what stops them drifting.

Each one is stated by the port itself; if a port raises its requirement
and nobody updates the launcher, the launcher will happily start a build
that cannot succeed.
"""
launcher = LAUNCHER.read_text()
declared = dict(re.findall(r"^\s+(\w+)\) minimum=([0-9.]+) ;;", launcher, re.M))
manifests = {
"go": (ROOT / "ports/go/go.mod", r"^go\s+([0-9.]+)"),
"rust": (ROOT / "ports/rust/Cargo.toml", r'rust-version\s*=\s*"([0-9.]+)"'),
"python": (ROOT / "ports/python/pyproject.toml", r'requires-python\s*=\s*">=\s*([0-9.]+)"'),
"ruby": (ROOT / "ports/ruby/hqtui.gemspec", r"required_ruby_version\s*=\s*'>=\s*([0-9.]+)'"),
"php": (ROOT / "ports/php/composer.json", r'"php":\s*">=\s*([0-9.]+)"'),
"cpp": (ROOT / "ports/cpp/CMakeLists.txt", r"cmake_minimum_required\(VERSION ([0-9.]+)"),
}
for language, (path, pattern) in manifests.items():
match = re.search(pattern, path.read_text(), re.M)
self.assertIsNotNone(match, f"no version found in {path}")
self.assertEqual(declared.get(language), match.group(1),
f"demo.sh says {language} needs {declared.get(language)}, "
f"but {path.name} says {match.group(1)}")
# Perl's manifest writes 5.020, which is 5.20.
perl = re.search(r"MIN_PERL_VERSION\s*=>\s*'([0-9.]+)'",
(ROOT / "ports/perl/Makefile.PL").read_text())
self.assertIsNotNone(perl)
self.assertEqual(declared.get("perl"),
re.sub(r"\.0*(\d)", r".\1", perl.group(1)))
# Bun's floor is not a preference; it is what can read a v2 lockfile.
self.assertEqual(declared.get("typescript"), "1.4.0")
self.assertIn('"lockfileVersion": 2', (ROOT / "bun.lock").read_text())

def test_bindings_update_both_managers_and_keep_builds_outside_source(self):
self.stub_compilers()
for manager in ('--system', '--mise'):
Expand Down
105 changes: 86 additions & 19 deletions apps/web/public/demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,41 @@ main() (
tool_spec=$tool
# Prebuilt PHP includes development headers for our small native adapter.
[ "$language" != php ] || tool_spec=conda:php
# A system toolchain older than this revision's pin does not fail here; it
# fails later, describing the wrong problem. Bun 1.3 cannot read a
# lockfileVersion 2 bun.lock, so it drops the lockfile and then reports
# frozen-lockfile drift, which reads as a broken repository. Compare the
# resolved driver against the pin up front and name both versions.
# What --mise installs is a pin, not a floor. Perl 5.40 and Ruby 3.3 build
# these bindings perfectly well, and treating the pin as a requirement makes
# --system useless on any machine that is not already pinned, which is most
# of them. So fail only below the version the port itself says it needs, and
# merely mention anything between that and the pin.
#
# Each floor is the one declared in that port's own manifest: go.mod,
# Cargo.toml rust-version, pyproject.toml requires-python, hqtui.gemspec,
# composer.json, Makefile.PL MIN_PERL_VERSION, CMakeLists.txt. Bun's is not
# a preference: 1.3 cannot parse a lockfileVersion 2 bun.lock, drops the
# lockfile, and then blames the repository for frozen-lockfile drift.
# apps/demo/scripts/test-updater.py checks this table against those files.
minimum=
case "$language" in
typescript) minimum=1.4.0 ;;
rust) minimum=1.75 ;;
go) minimum=1.22 ;;
python) minimum=3.10 ;;
zig) minimum=0.16.0 ;;
cpp) minimum=3.20 ;;
ruby) minimum=3.1 ;;
php) minimum=8.1 ;;
perl) minimum=5.20 ;;
esac
# Field-wise numeric comparison; absent trailing fields count as zero.
version_ge() {
awk -v have="$1" -v want="$2" 'BEGIN {
n = split(have, a, "."); m = split(want, b, "."); if (m > n) n = m
for (i = 1; i <= n; i++) {
if (a[i] + 0 > b[i] + 0) exit 0
if (a[i] + 0 < b[i] + 0) exit 1
}
exit 0
}'
}
if [ "$manager" = system ]; then
case "$language" in
typescript|rust|python|cpp|ruby) installed=$("$driver_path" --version 2>/dev/null) ;;
Expand All @@ -162,16 +192,11 @@ main() (
# line, whatever surrounds it ("go version go1.26.0", "cmake version 4.4.3").
installed=$(printf '%s\n' "$installed" | sed -n '1s/[^0-9]*\([0-9][0-9.]*\).*/\1/p' | sed 's/\.*$//')
[ -n "$installed" ] || fail "Cannot read the version of $driver_path. Use --mise to build against the pinned toolchain."
# Field-wise numeric comparison; absent trailing fields count as zero.
if ! awk -v have="$installed" -v want="$version" 'BEGIN {
n = split(have, a, "."); m = split(want, b, "."); if (m > n) n = m
for (i = 1; i <= n; i++) {
if (a[i] + 0 > b[i] + 0) exit 0
if (a[i] + 0 < b[i] + 0) exit 1
}
exit 0
}'; then
fail "This revision pins $tool $version, but $driver_path is $installed. Install $driver $version or newer, or rerun with --mise to build against the pinned toolchain."
if ! version_ge "$installed" "$minimum"; then
fail "$driver_path is $installed, and this demo needs $driver $minimum or newer. Upgrade $driver, or rerun with --mise to build against the pinned $tool $version."
fi
if ! version_ge "$installed" "$version"; then
note "Building with your $driver $installed; this revision pins $tool $version. Rerun with --mise if the build fails."
fi
fi
run_tool() {
Expand Down Expand Up @@ -203,12 +228,30 @@ main() (
cmake_version=$(awk '$1 == "cmake" && $2 == "=" {gsub(/"/, "", $3); print $3; exit}' "$source/mise.toml")
case "$cmake_version" in ''|*[!0-9.]*) fail 'Invalid CMake pin.' ;; esac
cmake_driver=cmake
cmake_via_mise=0
if [ "$manager" = system ]; then
cmake_driver=$(command -v cmake) || fail 'CMake is required. Install it or use --mise.'
case "$cmake_driver" in */mise/shims/*) cmake_driver=$(mise which cmake) || fail 'Activate CMake or use --mise.' ;; esac
if cmake_driver=$(command -v cmake 2>/dev/null); then
case "$cmake_driver" in */mise/shims/*) cmake_driver=$(mise which cmake) || fail 'Activate CMake or use --mise.' ;; esac
elif command -v mise >/dev/null 2>&1; then
# CMake is a build tool for the native binding, not the
# language runtime this flag is about, and the C++ compiler
# still comes from the host. Borrowing the pinned CMake beats
# refusing to run on a machine that has everything else.
note "No system CMake; borrowing the pinned CMake $cmake_version through mise."
cmake_via_mise=1
else
case "$(uname -s)" in
Darwin) fail 'CMake is needed to build the native binding. Install it with "brew install cmake", or install mise and rerun.' ;;
*) fail 'CMake is needed to build the native binding. Install it with your package manager (for example "apt install cmake"), or install mise and rerun.' ;;
esac
fi
fi
binding_cmake() {
if [ "$manager" = mise ]; then mise --no-config exec "cmake@$cmake_version" -- cmake "$@"; else "$cmake_driver" "$@"; fi
if [ "$manager" = mise ] || [ "$cmake_via_mise" -eq 1 ]; then
mise --no-config exec "cmake@$cmake_version" -- cmake "$@"
else
"$cmake_driver" "$@"
fi
}
binding_identity=$language-$version
php_config=
Expand Down Expand Up @@ -254,7 +297,31 @@ main() (
PERL5LIB=$perl_deps/lib/perl5${PERL5LIB:+:$PERL5LIB}; export PERL5LIB
if ! run_tool perl -MFFI::Platypus=2.11 -e '' 2>/dev/null; then
note 'Installing Perl FFI::Platypus into the private demo cache…'
run_tool cpanm --local-lib-contained "$perl_deps" --notest --mirror https://cpan.metacpan.org --mirror-only FFI::Platypus@2.11 >&2 || fail 'Install cpanm and native build tools, then retry. No global Perl modules were modified.'
if command -v cpanm >/dev/null 2>&1; then
run_tool cpanm --local-lib-contained "$perl_deps" --notest --mirror https://cpan.metacpan.org --mirror-only FFI::Platypus@2.11 >&2 \
|| fail 'FFI::Platypus did not build. A C compiler and make are required. No global Perl modules were modified.'
else
# macOS ships Perl but no cpanm, which stopped
# --system perl before it built anything. Fetch the
# standalone installer into the private cache rather
# than asking for a manual step; nothing global and
# nothing outside the cache is touched.
cpanm_script=$cache/deps/perl/cpanm
if [ ! -f "$cpanm_script" ]; then
mkdir -p "$cache/deps/perl"
note 'No cpanm found; fetching the standalone installer from cpanmin.us into the demo cache…'
if command -v curl >/dev/null 2>&1; then
curl -fsSL https://cpanmin.us -o "$cpanm_script.pending" || fail 'Could not download cpanm. Install cpanm, or use --mise.'
elif command -v wget >/dev/null 2>&1; then
wget -qO "$cpanm_script.pending" https://cpanmin.us || fail 'Could not download cpanm. Install cpanm, or use --mise.'
else
fail 'Fetching cpanm needs curl or wget. Install cpanm, or use --mise.'
fi
mv "$cpanm_script.pending" "$cpanm_script"
fi
run_tool perl "$cpanm_script" --local-lib-contained "$perl_deps" --notest --mirror https://cpan.metacpan.org --mirror-only FFI::Platypus@2.11 >&2 \
|| fail 'FFI::Platypus did not build. A C compiler and make are required. No global Perl modules were modified.'
fi
fi
launch perl "$source/ports/perl/examples/dashboard.pl" "$@"
;;
Expand Down