fix(install): pick the .tar.gz Node.js archive when the host has no xz
`tar xf node-*.tar.xz` shells out to the xz binary; minimal Debian, DietPi and WSL images ship tar without it, so extraction died mid-way and the installer then failed on a missing directory. Select .tar.xz only when `xz` is on PATH, in both the installer and the runtime node bootstrap. Same approach as the earlier #4229 (@JoshuaMart) and #39541 (@karnull); #11278 (@vominh1919) attempted an apt-only install of xz-utils instead. Refs #11197
This commit is contained in:
@@ -1042,12 +1042,18 @@ install_node_line() {
|
||||
|
||||
# Resolve the latest v${node_line}.x.x tarball name from the index page
|
||||
local index_url="https://nodejs.org/dist/latest-v${node_line}.x/"
|
||||
local tarball_name
|
||||
tarball_name=$(curl -fsSL "$index_url" \
|
||||
| grep -oE "node-v${node_line}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.xz" \
|
||||
| head -1)
|
||||
local tarball_name=""
|
||||
# `tar xf` shells out to xz for .tar.xz; minimal Debian/DietPi/WSL images ship tar without it
|
||||
# and the extract dies mid-way ("xz: Cannot exec"). Only pick .tar.xz when xz is present (#11197).
|
||||
if command -v xz >/dev/null 2>&1; then
|
||||
tarball_name=$(curl -fsSL "$index_url" \
|
||||
| grep -oE "node-v${node_line}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.xz" \
|
||||
| head -1)
|
||||
else
|
||||
log_info "xz not found — using the .tar.gz Node.js archive"
|
||||
fi
|
||||
|
||||
# Fallback to .tar.gz if .tar.xz not available
|
||||
# Fallback to .tar.gz if .tar.xz not available (or xz is missing)
|
||||
if [ -z "$tarball_name" ]; then
|
||||
tarball_name=$(curl -fsSL "$index_url" \
|
||||
| grep -oE "node-v${node_line}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.gz" \
|
||||
|
||||
@@ -271,10 +271,13 @@ _nb_install_bundled_node() {
|
||||
esac
|
||||
|
||||
local index_url="https://nodejs.org/dist/latest-v${HERMES_NODE_TARGET_MAJOR}.x/"
|
||||
local tarball
|
||||
tarball=$(curl -fsSL "$index_url" \
|
||||
| grep -oE "node-v${HERMES_NODE_TARGET_MAJOR}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.xz" \
|
||||
| head -1)
|
||||
local tarball=""
|
||||
# `tar xf` needs the xz binary for .tar.xz; minimal images ship tar without it (#11197).
|
||||
if command -v xz >/dev/null 2>&1; then
|
||||
tarball=$(curl -fsSL "$index_url" \
|
||||
| grep -oE "node-v${HERMES_NODE_TARGET_MAJOR}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.xz" \
|
||||
| head -1)
|
||||
fi
|
||||
if [ -z "$tarball" ]; then
|
||||
tarball=$(curl -fsSL "$index_url" \
|
||||
| grep -oE "node-v${HERMES_NODE_TARGET_MAJOR}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.gz" \
|
||||
|
||||
61
tests/test_install_sh_node_tarball_without_xz.py
Normal file
61
tests/test_install_sh_node_tarball_without_xz.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Node.js download picks a .tar.gz when the host has no ``xz`` (#11197).
|
||||
|
||||
`tar xf node-*.tar.xz` shells out to the xz binary; minimal Debian/DietPi/WSL images ship tar
|
||||
without it, so the extract died mid-way and the installer then failed on a missing directory.
|
||||
Both tarball selectors (installer and runtime bootstrap) are driven for real against a stubbed
|
||||
index page; only the ``xz`` binary's presence differs between the two arms.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
INDEX = "node-v26.7.0-linux-x64.tar.xz\nnode-v26.7.0-linux-x64.tar.gz\n"
|
||||
|
||||
|
||||
def _function(path: Path, name: str) -> str:
|
||||
match = re.search(rf"^{re.escape(name)}\(\) \{{\n.*?^\}}\n", path.read_text(encoding="utf-8"), re.M | re.S)
|
||||
assert match, f"{name}() not found in {path}"
|
||||
return match.group(0)
|
||||
|
||||
|
||||
def _selected_tarball(tmp_path: Path, *, with_xz: bool, script: Path, fn: str, call: str) -> str:
|
||||
"""Run the real tarball-selection function with curl stubbed to serve INDEX; the download step
|
||||
records the chosen name and aborts, so nothing is extracted."""
|
||||
bin_dir = tmp_path / ("bin-xz" if with_xz else "bin-noxz")
|
||||
bin_dir.mkdir()
|
||||
curl = bin_dir / "curl"
|
||||
curl.write_text("#!/bin/sh\nfor a in \"$@\"; do [ \"$a\" = -o ] && { echo \"$4\" >> \"$PICKED\"; exit 1; }; done\n"
|
||||
f"printf '%s' '{INDEX}'\n", encoding="utf-8")
|
||||
curl.chmod(curl.stat().st_mode | stat.S_IXUSR)
|
||||
# PATH holds only this dir: the host's real xz must not leak into the "no xz" arm.
|
||||
for tool in ("grep", "head", "mktemp", "rm", "uname", "sh", "printf"):
|
||||
real = shutil.which(tool)
|
||||
if real:
|
||||
os.symlink(real, bin_dir / tool)
|
||||
if with_xz:
|
||||
(bin_dir / "xz").write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
||||
(bin_dir / "xz").chmod(0o755)
|
||||
picked = tmp_path / f"picked-{with_xz}-{fn}"
|
||||
harness = ("log_info() { :; }; log_warn() { :; }; _nb_log() { :; }; _nb_warn() { :; }\n"
|
||||
"HERMES_NODE_TARGET_MAJOR=26\n" + _function(script, fn) + call)
|
||||
env = {"PATH": str(bin_dir), "PICKED": str(picked), "HOME": str(tmp_path)}
|
||||
subprocess.run([shutil.which("bash") or "/bin/bash", "-c", harness], env=env, check=False, capture_output=True)
|
||||
return picked.read_text(encoding="utf-8").strip() if picked.exists() else ""
|
||||
|
||||
|
||||
@pytest.mark.linux_only
|
||||
@pytest.mark.parametrize("script, fn, call", [
|
||||
(REPO_ROOT / "scripts" / "install.sh", "install_node_line", "\ninstall_node_line 26 linux x64\n"),
|
||||
(REPO_ROOT / "scripts" / "lib" / "node-bootstrap.sh", "_nb_install_bundled_node", "\n_nb_install_bundled_node\n"),
|
||||
])
|
||||
def test_tarball_format_follows_xz_availability(tmp_path: Path, script: Path, fn: str, call: str) -> None:
|
||||
assert _selected_tarball(tmp_path, with_xz=True, script=script, fn=fn, call=call).endswith(".tar.xz")
|
||||
assert _selected_tarball(tmp_path, with_xz=False, script=script, fn=fn, call=call).endswith(".tar.gz")
|
||||
Reference in New Issue
Block a user