Skip to content
Draft
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
12 changes: 12 additions & 0 deletions nvm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2329,6 +2329,18 @@ nvm_get_os() {
AIX\ *) NVM_OS=aix ;;
CYGWIN* | MSYS* | MINGW*) NVM_OS=win ;;
esac

# OpenHarmony can't be identified from `uname` alone: a device may run either
# a Linux kernel or a HarmonyOS kernel. Ask the userland, but only when
# `uname` has not already identified some other OS.
case "${NVM_OS-}" in
'' | linux)
if [ -x /system/bin/param ] && /system/bin/param get const.ohos.fullname </dev/null 2>/dev/null | nvm_grep -q '^OpenHarmony'; then
NVM_OS=openharmony
fi
;;
esac

nvm_echo "${NVM_OS-}"
}

Expand Down
160 changes: 160 additions & 0 deletions test/fast/Unit tests/nvm_get_os
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/bin/sh

# Save the PATH as it was when the test started to restore it when it finishes
ORIG_PATH="${PATH}"

cleanup() {
# Restore the PATH as it was when the test started
export PATH="${ORIG_PATH}"
rm -rf "${TMP_DIR}"
}

die() {
cleanup
echo "$@"
exit 1
}

: nvm.sh
\. ../../../nvm.sh

# Directory where mocked binaries used by nvm_get_os for each OS/arch are located
MOCKS_DIR="$(pwd)/../../mocks"

# Sets the PATH for these tests to include the symlinks to the mocked binaries
export PATH=".:${PATH}"

TMP_DIR=$(mktemp -d)

# Setups mock binaries for a given OS and arch that mimic the output of
# the real binaries used by nvm_get_os to guess the OS of a given system.
setup_mock_os() {
local OS
OS=$1
local ARCH
ARCH=$2
ln -sf "${MOCKS_DIR}/uname_${OS}_${ARCH}" ./uname
}

# Cleans up the setup done by setup_mock_os.
cleanup_mock_os() {
rm -f ./uname
}

# Runs nvm_get_os for OS $OS and arch $ARCH, and compares the expected
# output $EXPECTED_OUTPUT with the actual output. Does nothing and exits
# cleanly if they match, dies otherwise.
run_test() {
local EXPECTED
EXPECTED=$1
local OUTPUT
OUTPUT="$(nvm_get_os)"
[ "_${OUTPUT}" = "_${EXPECTED}" ] ||

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

die runs before the caller reaches cleanup_mock_os, and cleanup has no rm -f ./uname, so a failing arm leaves an executable symlink in this directory; urchin then runs it as a test on the next pass. nvm_get_arch tears down inside run_test, nvm_get_arch alpine puts rm -f ./uname in cleanup. Either is fine.

die "nvm_get_os produced \"${OUTPUT}\", expected \"${EXPECTED}\""
}

# Pre-existing arms

setup_mock_os linux aarch64
run_test "linux"
cleanup_mock_os

setup_mock_os osx amd64
run_test "darwin"
cleanup_mock_os

setup_mock_os smartos amd64
run_test "sunos"
cleanup_mock_os

# HarmonyOS kernel without OpenHarmony userland

setup_mock_os harmonyos aarch64
run_test ""
cleanup_mock_os

# OpenHarmony detection via chroot

setup_chroot() {
chroot_dir=$1
param_mock=$2
uname_mock=$3

# Directories
mkdir -p "${chroot_dir}/etc"
mkdir -p "${chroot_dir}/bin"
mkdir -p "${chroot_dir}/usr/bin"
mkdir -p "${chroot_dir}/lib64"
mkdir -p "${chroot_dir}/dev"
mkdir -p "${chroot_dir}/system/bin"

# Files and binaries
cp ../../../nvm.sh "${chroot_dir}/"
cp /bin/sh /usr/bin/dirname /usr/bin/grep "${chroot_dir}/usr/bin/"
cp "${uname_mock}" "${chroot_dir}/usr/bin/uname"
chmod +x "${chroot_dir}/usr/bin/uname"
ln -sf ../usr/bin/sh "${chroot_dir}/bin/sh"

# Fake OpenHarmony param binary
cp "${param_mock}" "${chroot_dir}/system/bin/param"
chmod +x "${chroot_dir}/system/bin/param"

# Libraries
for binary in /bin/sh /usr/bin/dirname /usr/bin/uname /usr/bin/grep; do
for lib in $(ldd $binary | awk '{print $3}' | grep "^/"); do
dir=$(dirname "${lib}")
mkdir -p "${chroot_dir}${dir}"
cp "${lib}" "${chroot_dir}${dir}/"
done
done

# Dynamic linker
cp /lib64/ld-linux-x86-64.so.2 "${chroot_dir}/lib64/"

# /dev/null
sudo mknod "${chroot_dir}/dev/null" c 1 3
}

# The chroot fixtures assume a glibc layout (fixed dynamic-linker path
# under /lib64, coreutils binaries). On musl Alpine that setup does not
# apply, and nvm_get_os's OpenHarmony probe cannot be exercised there;
# skip the chroot checks on Alpine.
if [ -f "/etc/alpine-release" ]; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This skips the whole block on every Alpine lane, and the four arms that remain pass byte-identically with the entire nvm.sh hunk deleted. 24 of the 32 fast jobs assert nothing about this PR, including the only arm64 coverage.

One arm closes most of that without a chroot, so it runs everywhere:

# openharmony + aarch64 composes the slug a mirror has to serve
ln -sf "${MOCKS_DIR}/uname_linux_aarch64" ./uname
nvm_get_os() { nvm_echo openharmony; }
SLUG="$(nvm_get_download_slug node binary v22.22.0)"
[ "${SLUG}" = 'node-v22.22.0-openharmony-arm64' ] || die "nvm_get_download_slug produced \"${SLUG}\""
unset nvm_get_os
rm -f ./uname

Checked in sh, bash, dash and zsh. It also pins that nvm_get_arch gives arm64 and not arm64-musl here, which the Alpine lanes are the only ones that could get wrong.

echo "on Alpine; skipping chroot OpenHarmony checks"
else
CHROOT_UNAME_LINUX_PARAM_OPENHARMONY="${TMP_DIR}/uname_linux_param_openharmony"
CHROOT_UNAME_LINUX_PARAM_OTHER="${TMP_DIR}/uname_linux_param_other"
CHROOT_UNAME_HARMONYOS_PARAM_OPENHARMONY="${TMP_DIR}/uname_harmonyos_param_openharmony"
CHROOT_UNAME_HARMONYOS_PARAM_OTHER="${TMP_DIR}/uname_harmonyos_param_other"

setup_chroot "${CHROOT_UNAME_LINUX_PARAM_OPENHARMONY}" "${MOCKS_DIR}/param_openharmony" "${MOCKS_DIR}/uname_linux_aarch64"
setup_chroot "${CHROOT_UNAME_LINUX_PARAM_OTHER}" "${MOCKS_DIR}/param_other" "${MOCKS_DIR}/uname_linux_aarch64"
setup_chroot "${CHROOT_UNAME_HARMONYOS_PARAM_OPENHARMONY}" "${MOCKS_DIR}/param_openharmony" "${MOCKS_DIR}/uname_harmonyos_aarch64"
setup_chroot "${CHROOT_UNAME_HARMONYOS_PARAM_OTHER}" "${MOCKS_DIR}/param_other" "${MOCKS_DIR}/uname_harmonyos_aarch64"

# Linux kernel with an OpenHarmony param -> openharmony
OS_UNAME_LINUX_PARAM_OPENHARMONY=$(sudo chroot "${CHROOT_UNAME_LINUX_PARAM_OPENHARMONY}" /bin/sh -c ". ./nvm.sh && nvm_get_os")
if [ "${OS_UNAME_LINUX_PARAM_OPENHARMONY}" != "openharmony" ]; then
die "nvm_get_os produced \"${OS_UNAME_LINUX_PARAM_OPENHARMONY}\", expected \"openharmony\""
fi

# Linux kernel with an unrelated param stays linux
OS_UNAME_LINUX_PARAM_OTHER=$(sudo chroot "${CHROOT_UNAME_LINUX_PARAM_OTHER}" /bin/sh -c ". ./nvm.sh && nvm_get_os")
if [ "${OS_UNAME_LINUX_PARAM_OTHER}" != "linux" ]; then
die "nvm_get_os produced \"${OS_UNAME_LINUX_PARAM_OTHER}\", expected \"linux\""
fi

# HarmonyOS kernel with an OpenHarmony param -> openharmony
OS_UNAME_HARMONYOS_PARAM_OPENHARMONY=$(sudo chroot "${CHROOT_UNAME_HARMONYOS_PARAM_OPENHARMONY}" /bin/sh -c ". ./nvm.sh && nvm_get_os")
if [ "${OS_UNAME_HARMONYOS_PARAM_OPENHARMONY}" != "openharmony" ]; then
die "nvm_get_os produced \"${OS_UNAME_HARMONYOS_PARAM_OPENHARMONY}\", expected \"openharmony\""
fi

# HarmonyOS kernel with an unrelated param stays empty
OS_UNAME_HARMONYOS_PARAM_OTHER=$(sudo chroot "${CHROOT_UNAME_HARMONYOS_PARAM_OTHER}" /bin/sh -c ". ./nvm.sh && nvm_get_os")
if [ "_${OS_UNAME_HARMONYOS_PARAM_OTHER}" != "_" ]; then
die "nvm_get_os produced \"${OS_UNAME_HARMONYOS_PARAM_OTHER}\", expected \"\""
fi

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing pins the '' | linux guard: delete the case wrapper so the probe runs unconditionally, and all 8 assertions still pass. That is the property the last round asked for.

One more fixture covers it:

  CHROOT_UNAME_OSX_PARAM_OPENHARMONY="${TMP_DIR}/uname_osx_param_openharmony"
  setup_chroot "${CHROOT_UNAME_OSX_PARAM_OPENHARMONY}" "${MOCKS_DIR}/param_openharmony" "${MOCKS_DIR}/uname_osx_amd64"

  # an OS uname already identified is never re-probed
  OS_UNAME_OSX_PARAM_OPENHARMONY=$(sudo chroot "${CHROOT_UNAME_OSX_PARAM_OPENHARMONY}" /bin/sh -c ". ./nvm.sh && nvm_get_os")
  if [ "${OS_UNAME_OSX_PARAM_OPENHARMONY}" != "darwin" ]; then
    die "nvm_get_os produced \"${OS_UNAME_OSX_PARAM_OPENHARMONY}\", expected \"darwin\""
  fi

fi

cleanup

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is the first unit test nvm_get_os has ever had, it'd be good to lock in more than the two arms the PR touches. Three additions, all cheap:

  1. HarmonyOS uname with no param - asserts that the new mock alone isn't what produces openharmony, and pins the empty-string result:

    # HarmonyOS kernel without the OpenHarmony userland
    ln -sf "${MOCKS_DIR}/uname_harmonyos_aarch64" ./uname
    rm -f ./param
    run_test ""
  2. Non-Linux host with param present - this is the case my other comment is about; once the probe is gated it should assert darwin, which is exactly the regression test for it:

    ln -sf "${MOCKS_DIR}/uname_osx_amd64" ./uname
    ln -sf "${MOCKS_DIR}/param_openharmony" ./param
    run_test "darwin"
  3. The pre-existing arms - uname_osx_amd64 and uname_smartos_amd64 already exist in test/mocks, so darwin and sunos are one ln -sf each. Right now five of nvm_get_os's seven case arms are untested.

Also worth knowing (not a defect, just a footnote): urchin expands the directory glob once before running, so the transient ./uname and ./param symlinks can't be picked up as tests mid-run. But a run interrupted between ln -sf ./param and cleanup leaves a ./param behind, and since . is on PATH for this directory, every later nvm_get_os call in it would report openharmony. Your cleanup is already stricter than nvm_get_arch's about this; just something to keep in mind if a local run ever goes weird.

3 changes: 3 additions & 0 deletions test/mocks/param_openharmony
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if [ "$#" -eq 2 ] && [ "$1" = "get" ] && [ "$2" = "const.ohos.fullname" ]; then
echo "OpenHarmony-6.1.0.31"
fi
3 changes: 3 additions & 0 deletions test/mocks/param_other
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if [ "$#" -eq 2 ] && [ "$1" = "get" ] && [ "$2" = "const.ohos.fullname" ]; then
echo "SomeOtherOS-1.2.3"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth making this NotOpenHarmony-1.2.3. As-is it shares no substring with OpenHarmony, so dropping the ^ from nvm_grep -q '^OpenHarmony' leaves all 8 assertions green. Changing the string keeps both existing expectations and makes them pin the anchor.

fi
5 changes: 5 additions & 0 deletions test/mocks/uname_harmonyos_aarch64
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if [ "_$1" = "_-m" ]; then
echo "aarch64"
else
echo "HarmonyOS localhost HongMeng Kernel 1.12.0 #1 SMP Tue Jul 14 09:40:51 UTC 2026 aarch64 Toybox"
fi
Loading