diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-02-28 22:00:31 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-02-28 22:00:31 +0300 |
| commit | 3eb3441bfe6c4d780a798af490046d2d88211705 (patch) | |
| tree | e0bdfa4051a3de4ab0e62f2a4349af7fdbc88022 | |
| download | Project-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-- | .gitignore | 25 | ||||
| -rw-r--r-- | GNUmakefile | 37 | ||||
| -rw-r--r-- | README.md | 29 | ||||
| -rw-r--r-- | linux-version.1 | 124 | ||||
| -rw-r--r-- | linux-version.sh.in | 377 | ||||
| -rw-r--r-- | tests/test.sh | 125 |
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" |
