summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-02-28 22:00:31 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-02-28 22:00:31 +0300
commit3eb3441bfe6c4d780a798af490046d2d88211705 (patch)
treee0bdfa4051a3de4ab0e62f2a4349af7fdbc88022
downloadProject-Tick-3eb3441bfe6c4d780a798af490046d2d88211705.tar.gz
Project-Tick-3eb3441bfe6c4d780a798af490046d2d88211705.zip
init Standalone shell-based Linux port of FreeBSD `freebsd-version`, renamed to `linux-version` for this repository.
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
-rw-r--r--.gitignore25
-rw-r--r--GNUmakefile37
-rw-r--r--README.md29
-rw-r--r--linux-version.1124
-rw-r--r--linux-version.sh.in377
-rw-r--r--tests/test.sh125
6 files changed, 717 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..a74d30b48c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+*.a
+*.core
+*.lo
+*.nossppico
+*.o
+*.orig
+*.pico
+*.pieo
+*.po
+*.rej
+*.so
+*.so.[0-9]*
+*.sw[nop]
+*~
+.*DS_Store
+.cache
+.clangd
+.ccls-cache
+.depend*
+compile_commands.json
+compile_commands.events.json
+tags
+build/
+out/
+.linux-obj/
diff --git a/GNUmakefile b/GNUmakefile
new file mode 100644
index 0000000000..21357b27a3
--- /dev/null
+++ b/GNUmakefile
@@ -0,0 +1,37 @@
+.DEFAULT_GOAL := all
+
+SED ?= sed
+SH ?= sh
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+GENERATED := $(OBJDIR)/linux-version
+TARGET := $(OUTDIR)/linux-version
+
+.PHONY: all clean dirs status test
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(GENERATED): $(CURDIR)/linux-version.sh.in | dirs
+ $(SED) \
+ -e 's|@@OS_RELEASE_PRIMARY@@|/etc/os-release|g' \
+ -e 's|@@OS_RELEASE_FALLBACK@@|/usr/lib/os-release|g' \
+ -e 's|@@PROC_OSRELEASE@@|/proc/sys/kernel/osrelease|g' \
+ "$<" >"$@"
+ @chmod +x "$@"
+
+$(TARGET): $(GENERATED) | dirs
+ cp "$(GENERATED)" "$(TARGET)"
+ @chmod +x "$(TARGET)"
+
+test: $(TARGET)
+ LINUX_VERSION_BIN="$(TARGET)" $(SH) "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(OBJDIR)" "$(OUTDIR)"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..aeb1d1aca3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# linux-version
+
+Standalone shell-based Linux port of FreeBSD `freebsd-version`, renamed to `linux-version` for this repository.
+
+## Build
+
+```sh
+gmake -f GNUmakefile
+gmake -f GNUmakefile CC=musl-gcc
+```
+
+This port is implemented as a POSIX `sh` script, so the `CC=musl-gcc` build is a reproducibility check rather than a separate compilation path.
+
+## Test
+
+```sh
+gmake -f GNUmakefile test
+gmake -f GNUmakefile test CC=musl-gcc
+```
+
+## Notes
+
+- Port strategy is Linux-native translation, not preservation of FreeBSD loader, `sysctl`, or jail semantics.
+- `-r` reads the running kernel release from `/proc/sys/kernel/osrelease`, with `uname -r` only as a fallback when procfs is unavailable.
+- `-u` reads the installed userland version from the target root's `os-release` file. The port prefers `VERSION_ID`, then falls back to `BUILD_ID`, `VERSION`, and `PRETTY_NAME`.
+- `-k` does not inspect a FreeBSD kernel binary. On Linux it resolves the default booted kernel via `/vmlinuz` or `/boot/vmlinuz` symlinks when available, then falls back to a unique kernel release under `/lib/modules` or `/usr/lib/modules`.
+- `ROOT=/path` is supported for offline inspection of another Linux filesystem tree for `-k` and `-u`.
+- Unsupported semantics are explicit: `-j` is rejected because Linux containers and namespaces do not provide a jail-equivalent installed-userland query with compatible semantics.
+- The port does not guess when several kernel module trees exist without a default boot symlink; it fails with an explicit ambiguity error instead.
diff --git a/linux-version.1 b/linux-version.1
new file mode 100644
index 0000000000..97a992586e
--- /dev/null
+++ b/linux-version.1
@@ -0,0 +1,124 @@
+.\"-
+.\" Copyright (c) 2026
+.\" Project Tick. All rights reserved.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.Dd February 28, 2026
+.Dt LINUX-VERSION 1
+.Os
+.Sh NAME
+.Nm linux-version
+.Nd print the installed system and kernel version on Linux
+.Sh SYNOPSIS
+.Nm
+.Op Fl kru
+.Op Fl j Ar jail
+.Sh DESCRIPTION
+The
+.Nm
+utility is a Linux-native port of
+.Xr freebsd-version 1
+for this repository.
+It reports the installed kernel version,
+the running kernel version,
+and the installed userland version.
+.Pp
+The following options are available:
+.Bl -tag -width Fl
+.It Fl k
+Print the installed kernel version for the target root.
+On Linux, this is resolved from the default kernel symlink
+.Pa /vmlinuz
+or
+.Pa /boot/vmlinuz
+when available,
+otherwise from a unique kernel release under
+.Pa /lib/modules
+or
+.Pa /usr/lib/modules .
+If several installed kernels exist and no default boot target can be
+determined, the command fails with an explicit error.
+.It Fl r
+Print the running kernel version from
+.Pa /proc/sys/kernel/osrelease .
+.It Fl u
+Print the installed userland version from
+.Pa /etc/os-release
+or
+.Pa /usr/lib/os-release
+under the target root.
+The utility prefers
+.Ev VERSION_ID ,
+then falls back to
+.Ev BUILD_ID ,
+.Ev VERSION ,
+and
+.Ev PRETTY_NAME .
+.It Fl j Ar jail
+Accepted for interface compatibility but not supported on Linux.
+The command exits with an explicit error because Linux containers and
+namespaces do not provide a jail-equivalent query with compatible semantics.
+.El
+.Pp
+If several supported options are specified,
+.Nm
+prints the installed kernel version first,
+then the running kernel version,
+and finally the installed userland version,
+each on a separate line.
+If no option is specified,
+it prints the installed userland version only.
+.Sh ENVIRONMENT
+.Bl -tag -width ROOT
+.It Ev ROOT
+Path to the root of the filesystem tree in which to inspect
+.Pa os-release ,
+.Pa /boot/vmlinuz ,
+and kernel module directories for
+.Fl k
+and
+.Fl u .
+This does not affect
+.Fl r ,
+which always reports the current running kernel.
+.El
+.Sh IMPLEMENTATION NOTES
+This port intentionally does not preserve FreeBSD-specific implementation
+details such as parsing
+.Pa loader.conf ,
+querying
+.Va kern.osrelease
+via
+.Xr sysctl 8 ,
+or executing inside jails with
+.Xr jexec 8 .
+.Pp
+The Linux kernel does not provide a syscall or procfs interface that reports
+the installed-on-disk kernel version separately from the running kernel.
+Therefore
+.Nm
+uses the default kernel symlink when present and otherwise requires a unique
+kernel release under the module tree.
+.Sh SEE ALSO
+.Xr uname 1 ,
+.Xr os-release 5
diff --git a/linux-version.sh.in b/linux-version.sh.in
new file mode 100644
index 0000000000..52cd486f96
--- /dev/null
+++ b/linux-version.sh.in
@@ -0,0 +1,377 @@
+#!/bin/sh
+
+set -eu
+
+: "${ROOT:=}"
+: "${OS_RELEASE_PRIMARY:=@@OS_RELEASE_PRIMARY@@}"
+: "${OS_RELEASE_FALLBACK:=@@OS_RELEASE_FALLBACK@@}"
+: "${PROC_OSRELEASE:=@@PROC_OSRELEASE@@}"
+
+progname=${0##*/}
+
+error() {
+ printf '%s\n' "$progname: $*" >&2
+ exit 1
+}
+
+usage() {
+ printf '%s\n' "usage: $progname [-kru] [-j jail]" >&2
+ exit 1
+}
+
+root_path() {
+ case $1 in
+ /*)
+ if [ -n "$ROOT" ]; then
+ printf '%s%s\n' "$ROOT" "$1"
+ else
+ printf '%s\n' "$1"
+ fi
+ ;;
+ *)
+ error "internal error: expected absolute path, got '$1'"
+ ;;
+ esac
+}
+
+path_basename() {
+ path=$1
+ while [ "$path" != "/" ] && [ "${path%/}" != "$path" ]; do
+ path=${path%/}
+ done
+ printf '%s\n' "${path##*/}"
+}
+
+extract_release_from_name() {
+ case $1 in
+ vmlinuz-*)
+ printf '%s\n' "${1#vmlinuz-}"
+ return 0
+ ;;
+ bzImage-*)
+ printf '%s\n' "${1#bzImage-}"
+ return 0
+ ;;
+ kernel-*)
+ printf '%s\n' "${1#kernel-}"
+ return 0
+ ;;
+ esac
+ return 1
+}
+
+append_unique_candidate() {
+ candidate=$1
+ [ -n "$candidate" ] || return 0
+
+ if [ -z "${kernel_candidates:-}" ]; then
+ kernel_candidates=$candidate
+ return 0
+ fi
+
+ if printf '%s\n' "$kernel_candidates" | grep -Fx "$candidate" >/dev/null 2>&1; then
+ return 0
+ fi
+
+ kernel_candidates=$kernel_candidates'
+'$candidate
+}
+
+extract_release_from_link() {
+ link_path=$1
+
+ [ -L "$link_path" ] || return 1
+ target=$(readlink "$link_path" 2>/dev/null) || return 1
+ extract_release_from_name "$(path_basename "$target")"
+}
+
+collect_boot_kernel_candidates() {
+ for absolute in /vmlinuz /boot/vmlinuz; do
+ link_path=$(root_path "$absolute")
+ if release=$(extract_release_from_link "$link_path"); then
+ printf '%s\n' "$release"
+ return 0
+ fi
+ done
+
+ for absolute in /vmlinuz-* /boot/vmlinuz-* /boot/bzImage-* /boot/kernel-*; do
+ pattern=$(root_path "$absolute")
+ for entry in $pattern; do
+ [ -e "$entry" ] || continue
+ release=$(extract_release_from_name "$(path_basename "$entry")") || continue
+ append_unique_candidate "$release"
+ done
+ done
+
+ return 1
+}
+
+collect_module_tree_candidates() {
+ for absolute in /lib/modules /usr/lib/modules; do
+ modules_root=$(root_path "$absolute")
+ [ -d "$modules_root" ] || continue
+
+ for entry in "$modules_root"/*; do
+ [ -d "$entry" ] || continue
+ if [ ! -d "$entry/kernel" ] && [ ! -f "$entry/modules.dep" ]; then
+ continue
+ fi
+ append_unique_candidate "$(path_basename "$entry")"
+ done
+ done
+}
+
+format_candidate_list() {
+ printf '%s\n' "$kernel_candidates" | awk '
+ NF {
+ if (count > 0)
+ printf(", ");
+ printf("%s", $0);
+ count++;
+ }
+ END {
+ if (count > 0)
+ printf("\n");
+ }
+ '
+}
+
+installed_kernel_version() {
+ kernel_candidates=
+
+ if release=$(collect_boot_kernel_candidates); then
+ printf '%s\n' "$release"
+ return 0
+ fi
+
+ collect_module_tree_candidates
+ count=$(printf '%s\n' "${kernel_candidates:-}" | awk 'NF { count++ } END { print count + 0 }')
+
+ case $count in
+ 0)
+ error "unable to determine installed kernel version under '${ROOT:-/}'"
+ ;;
+ 1)
+ printf '%s\n' "$kernel_candidates"
+ ;;
+ *)
+ error "unable to determine installed kernel version under '${ROOT:-/}': multiple kernel releases found ($(format_candidate_list))"
+ ;;
+ esac
+}
+
+running_kernel_version() {
+ if [ -r "$PROC_OSRELEASE" ]; then
+ release=
+ IFS= read -r release <"$PROC_OSRELEASE" || true
+ [ -n "$release" ] || error "unable to determine running kernel version from $PROC_OSRELEASE"
+ printf '%s\n' "$release"
+ return 0
+ fi
+
+ if command -v uname >/dev/null 2>&1; then
+ command uname -r
+ return 0
+ fi
+
+ error "unable to determine running kernel version: $PROC_OSRELEASE is unavailable"
+}
+
+extract_os_release_field() {
+ field=$1
+ file=$2
+
+ if output=$(awk -v want="$field" '
+ function ltrim(s) {
+ sub(/^[[:space:]]+/, "", s)
+ return s
+ }
+
+ function rtrim(s) {
+ sub(/[[:space:]]+$/, "", s)
+ return s
+ }
+
+ function decode_quoted(s, out, i, c, esc) {
+ out = ""
+ esc = 0
+ for (i = 2; i <= length(s); i++) {
+ c = substr(s, i, 1)
+ if (esc) {
+ out = out c
+ esc = 0
+ continue
+ }
+ if (c == "\\") {
+ esc = 1
+ continue
+ }
+ if (c == "\"") {
+ if (rtrim(substr(s, i + 1)) != "")
+ return "__TRAILING__"
+ return out
+ }
+ out = out c
+ }
+ return "__UNTERMINATED__"
+ }
+
+ BEGIN {
+ found = 0
+ parse_error = 0
+ }
+
+ /^[[:space:]]*#/ || /^[[:space:]]*$/ {
+ next
+ }
+
+ {
+ line = $0
+ sub(/^[[:space:]]+/, "", line)
+ if (line !~ /^[A-Z0-9_]+=.*$/)
+ next
+
+ key = line
+ sub(/=.*/, "", key)
+ if (key != want)
+ next
+
+ value = line
+ sub(/^[^=]*=/, "", value)
+ value = ltrim(value)
+
+ if (value ~ /^"/) {
+ value = decode_quoted(value)
+ if (value == "__TRAILING__" || value == "__UNTERMINATED__") {
+ parse_error = 1
+ exit
+ }
+ print value
+ found = 1
+ exit 0
+ }
+
+ if (value ~ /[[:space:]]/) {
+ parse_error = 1
+ exit
+ }
+
+ print rtrim(value)
+ found = 1
+ exit 0
+ }
+
+ END {
+ if (parse_error)
+ exit 3
+ if (!found)
+ exit 1
+ }
+ ' "$file" 2>&1); then
+ status=0
+ else
+ status=$?
+ fi
+
+ case $status in
+ 0)
+ [ -n "$output" ] || return 1
+ printf '%s\n' "$output"
+ return 0
+ ;;
+ 1)
+ return 1
+ ;;
+ 3)
+ return 2
+ ;;
+ *)
+ return 3
+ ;;
+ esac
+}
+
+userland_version() {
+ primary=$(root_path "$OS_RELEASE_PRIMARY")
+ fallback=$(root_path "$OS_RELEASE_FALLBACK")
+
+ for file in "$primary" "$fallback"; do
+ [ -r "$file" ] || continue
+
+ for field in VERSION_ID BUILD_ID VERSION PRETTY_NAME; do
+ if value=$(extract_os_release_field "$field" "$file"); then
+ printf '%s\n' "$value"
+ return 0
+ else
+ status=$?
+ case $status in
+ 1)
+ ;;
+ 2)
+ error "malformed os-release entry '$field' in $file"
+ ;;
+ *)
+ error "failed to parse $file"
+ ;;
+ esac
+ fi
+ done
+
+ error "unable to determine userland version from $file"
+ done
+
+ error "unable to locate os-release under '${ROOT:-/}'"
+}
+
+main() {
+ opt_k=0
+ opt_r=0
+ opt_u=0
+ opt_j=0
+
+ OPTIND=1
+ while getopts "kruj:" option; do
+ case $option in
+ k)
+ opt_k=1
+ ;;
+ r)
+ opt_r=1
+ ;;
+ u)
+ opt_u=1
+ ;;
+ j)
+ opt_j=1
+ ;;
+ *)
+ usage
+ ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+
+ [ $# -eq 0 ] || usage
+
+ if [ $((opt_k + opt_r + opt_u + opt_j)) -eq 0 ]; then
+ opt_u=1
+ fi
+
+ if [ "$opt_j" -ne 0 ]; then
+ error "option -j is not supported on Linux"
+ fi
+
+ if [ "$opt_k" -ne 0 ]; then
+ installed_kernel_version
+ fi
+
+ if [ "$opt_r" -ne 0 ]; then
+ running_kernel_version
+ fi
+
+ if [ "$opt_u" -ne 0 ]; then
+ userland_version
+ fi
+}
+
+main "$@"
diff --git a/tests/test.sh b/tests/test.sh
new file mode 100644
index 0000000000..ea0bccc7d8
--- /dev/null
+++ b/tests/test.sh
@@ -0,0 +1,125 @@
+#!/bin/sh
+set -eu
+
+ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+LINUX_VERSION_BIN=${LINUX_VERSION_BIN:-"$ROOT_DIR/out/linux-version"}
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/linux-version-test.XXXXXX")
+trap 'rm -rf "$WORKDIR"' EXIT INT TERM
+
+fail() {
+ printf '%s\n' "FAIL: $1" >&2
+ exit 1
+}
+
+assert_eq() {
+ name=$1
+ expected=$2
+ actual=$3
+ if [ "$expected" != "$actual" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "--- expected ---" >&2
+ printf '%s\n' "$expected" >&2
+ printf '%s\n' "--- actual ---" >&2
+ printf '%s\n' "$actual" >&2
+ exit 1
+ fi
+}
+
+assert_match() {
+ name=$1
+ pattern=$2
+ text=$3
+ printf '%s\n' "$text" | grep -Eq "$pattern" || fail "$name"
+}
+
+write_os_release() {
+ root=$1
+ content=$2
+ mkdir -p "$root/etc"
+ printf '%s\n' "$content" >"$root/etc/os-release"
+}
+
+[ -x "$LINUX_VERSION_BIN" ] || fail "missing binary: $LINUX_VERSION_BIN"
+
+usage_output=$("$LINUX_VERSION_BIN" -x 2>&1 || true)
+assert_match "invalid option should print usage" '^usage: linux-version ' "$usage_output"
+
+too_many_output=$("$LINUX_VERSION_BIN" arg1 arg2 2>&1 || true)
+assert_match "extra operands should print usage" '^usage: linux-version ' "$too_many_output"
+
+jail_output=$("$LINUX_VERSION_BIN" -j demo 2>&1 || true)
+assert_eq "unsupported jail option" \
+ "linux-version: option -j is not supported on Linux" "$jail_output"
+
+root_default="$WORKDIR/root-default"
+write_os_release "$root_default" 'NAME=Test Linux
+VERSION_ID=42.3
+PRETTY_NAME="Test Linux 42.3"'
+
+default_output=$(ROOT="$root_default" "$LINUX_VERSION_BIN")
+assert_eq "default output should be userland version" "42.3" "$default_output"
+
+quoted_root="$WORKDIR/root-quoted"
+write_os_release "$quoted_root" 'NAME=Quoted Linux
+PRETTY_NAME="Quoted Linux 1.0 LTS"'
+
+quoted_output=$(ROOT="$quoted_root" "$LINUX_VERSION_BIN" -u)
+assert_eq "quoted PRETTY_NAME fallback" "Quoted Linux 1.0 LTS" "$quoted_output"
+
+malformed_root="$WORKDIR/root-malformed"
+write_os_release "$malformed_root" 'NAME=Broken Linux
+VERSION_ID="unterminated'
+
+malformed_output=$(ROOT="$malformed_root" "$LINUX_VERSION_BIN" -u 2>&1 || true)
+assert_eq "malformed os-release should fail" \
+ "linux-version: malformed os-release entry 'VERSION_ID' in $malformed_root/etc/os-release" \
+ "$malformed_output"
+
+missing_root="$WORKDIR/root-missing"
+mkdir -p "$missing_root"
+missing_output=$(ROOT="$missing_root" "$LINUX_VERSION_BIN" -u 2>&1 || true)
+assert_eq "missing os-release should fail" \
+ "linux-version: unable to locate os-release under '$missing_root'" "$missing_output"
+
+running_expected=
+if [ -r /proc/sys/kernel/osrelease ]; then
+ IFS= read -r running_expected </proc/sys/kernel/osrelease || true
+fi
+if [ -z "$running_expected" ]; then
+ running_expected=$(uname -r)
+fi
+running_output=$("$LINUX_VERSION_BIN" -r)
+assert_eq "running kernel release" "$running_expected" "$running_output"
+
+kernel_link_root="$WORKDIR/root-kernel-link"
+mkdir -p "$kernel_link_root/boot" "$kernel_link_root/etc"
+ln -s ../images/vmlinuz-6.9.7-port "$kernel_link_root/boot/vmlinuz"
+write_os_release "$kernel_link_root" 'VERSION_ID=9.1'
+
+kernel_link_output=$(ROOT="$kernel_link_root" "$LINUX_VERSION_BIN" -k)
+assert_eq "kernel version from default symlink" "6.9.7-port" "$kernel_link_output"
+
+kernel_modules_root="$WORKDIR/root-kernel-modules"
+mkdir -p "$kernel_modules_root/lib/modules/6.8.12-demo/kernel" "$kernel_modules_root/etc"
+: >"$kernel_modules_root/lib/modules/6.8.12-demo/modules.dep"
+write_os_release "$kernel_modules_root" 'VERSION_ID=6.8-userland'
+
+kernel_modules_output=$(ROOT="$kernel_modules_root" "$LINUX_VERSION_BIN" -k)
+assert_eq "kernel version from unique modules tree" "6.8.12-demo" "$kernel_modules_output"
+
+ambiguous_root="$WORKDIR/root-ambiguous"
+mkdir -p "$ambiguous_root/lib/modules/6.1.1-a/kernel" "$ambiguous_root/lib/modules/6.1.2-b/kernel"
+: >"$ambiguous_root/lib/modules/6.1.1-a/modules.dep"
+: >"$ambiguous_root/lib/modules/6.1.2-b/modules.dep"
+
+ambiguous_output=$(ROOT="$ambiguous_root" "$LINUX_VERSION_BIN" -k 2>&1 || true)
+assert_match "ambiguous modules tree should fail" \
+ "^linux-version: unable to determine installed kernel version under '$ambiguous_root': multiple kernel releases found \\(6\\.1\\.1-a, 6\\.1\\.2-b\\)$" \
+ "$ambiguous_output"
+
+combo_output=$(ROOT="$kernel_link_root" "$LINUX_VERSION_BIN" -kru)
+combo_expected=$(printf '%s\n%s\n%s' "6.9.7-port" "$running_expected" "9.1")
+assert_eq "combined output order" "$combo_expected" "$combo_output"
+
+printf '%s\n' "PASS"