diff -pruN 1.7.8-1/.github/workflows/dragonflybsd.yml 1.7.9-1/.github/workflows/dragonflybsd.yml
--- 1.7.8-1/.github/workflows/dragonflybsd.yml	1970-01-01 00:00:00.000000000 +0000
+++ 1.7.9-1/.github/workflows/dragonflybsd.yml	2025-11-25 14:31:14.000000000 +0000
@@ -0,0 +1,34 @@
+name: DragonflyBSD
+
+on:
+  pull_request:
+  push:
+  release:
+    types: [published]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    concurrency:
+        group: ${{ github.ref }}-${{ github.base_ref }}-${{ github.head_ref }}-DragonflyBSD
+        cancel-in-progress: true
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: true
+
+      - name: Tests
+        id: test
+        uses: vmactions/dragonflybsd-vm@v1
+        with:
+          usesh: true
+          prepare: |
+            pkg install -y socat git gmake bash
+          run: |
+            cd $GITHUB_WORKSPACE
+            ./configure
+            gmake -j4
+            gmake run
+
diff -pruN 1.7.8-1/.github/workflows/freebsd.yml 1.7.9-1/.github/workflows/freebsd.yml
--- 1.7.8-1/.github/workflows/freebsd.yml	1970-01-01 00:00:00.000000000 +0000
+++ 1.7.9-1/.github/workflows/freebsd.yml	2025-11-25 14:31:14.000000000 +0000
@@ -0,0 +1,33 @@
+name: FreeBSD
+
+on:
+  pull_request:
+  push:
+  release:
+    types: [published]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    concurrency:
+        group: ${{ github.ref }}-${{ github.base_ref }}-${{ github.head_ref }}-FreeBSD
+        cancel-in-progress: true
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          submodules: true
+
+      - name: Tests
+        uses: vmactions/freebsd-vm@v1
+        with:
+          usesh: true
+          mem: 4096
+          copyback: false
+          prepare: pkg install -y git curl unzip gmake llvm gsed bash perl5
+          run: |
+            ./configure
+            gmake -j4
+            gmake run
+
diff -pruN 1.7.8-1/.github/workflows/netbsd.yml 1.7.9-1/.github/workflows/netbsd.yml
--- 1.7.8-1/.github/workflows/netbsd.yml	1970-01-01 00:00:00.000000000 +0000
+++ 1.7.9-1/.github/workflows/netbsd.yml	2025-11-25 14:31:14.000000000 +0000
@@ -0,0 +1,34 @@
+name: NetBSD
+
+on:
+  pull_request:
+  push:
+  release:
+    types: [published]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    concurrency:
+        group: ${{ github.ref }}-${{ github.base_ref }}-${{ github.head_ref }}-NetBSD
+        cancel-in-progress: true
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: true
+
+      - name: Tests
+        id: test
+        uses: vmactions/netbsd-vm@v1
+        with:
+          usesh: true
+          prepare: |
+            /usr/sbin/pkg_add curl bash git gmake
+          run: |
+            cd $GITHUB_WORKSPACE
+            ./configure
+            gmake -j4
+            gmake run
+
diff -pruN 1.7.8-1/.github/workflows/openbsd.yml 1.7.9-1/.github/workflows/openbsd.yml
--- 1.7.8-1/.github/workflows/openbsd.yml	1970-01-01 00:00:00.000000000 +0000
+++ 1.7.9-1/.github/workflows/openbsd.yml	2025-11-25 14:31:14.000000000 +0000
@@ -0,0 +1,35 @@
+name: OpenBSD
+
+on:
+  pull_request:
+  push:
+  release:
+    types: [published]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    concurrency:
+        group: ${{ github.ref }}-${{ github.base_ref }}-${{ github.head_ref }}-OpenBSD
+        cancel-in-progress: true
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: true
+
+      - name: Tests
+        id: test
+        uses: vmactions/openbsd-vm@v1
+        with:
+          usesh: true
+          prepare: |
+            pkg_add curl git gmake bash gcc
+            ln -sf /usr/local/bin/gmake /usr/local/bin/make || true
+          run: |
+            cd $GITHUB_WORKSPACE
+            ./configure
+            gmake -j4
+            gmake run
+
diff -pruN 1.7.8-1/.github/workflows/solaris.yml 1.7.9-1/.github/workflows/solaris.yml
--- 1.7.8-1/.github/workflows/solaris.yml	1970-01-01 00:00:00.000000000 +0000
+++ 1.7.9-1/.github/workflows/solaris.yml	2025-11-25 14:31:14.000000000 +0000
@@ -0,0 +1,34 @@
+name: Solaris
+
+on:
+  pull_request:
+  push:
+  release:
+    types: [published]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    concurrency:
+        group: ${{ github.ref }}-${{ github.base_ref }}-${{ github.head_ref }}-Solaris
+        cancel-in-progress: true
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: true
+
+      - name: Tests
+        id: test
+        uses: vmactions/solaris-vm@v1
+        with:
+          usesh: true
+          prepare: |
+            pkgutil -y -i socat git gmake bash gcc4g++ || pkgutil -y -i socat git gmake bash gcc5g++ || pkg install -y developer/gcc || true
+          run: |
+            cd $GITHUB_WORKSPACE
+            bash ./configure
+            gmake -j4
+            gmake run
+
diff -pruN 1.7.8-1/.gitignore 1.7.9-1/.gitignore
--- 1.7.8-1/.gitignore	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/.gitignore	2025-11-25 14:31:14.000000000 +0000
@@ -36,3 +36,4 @@ Doxyfile
 CMakeLists.txt
 Makefile
 makefile
+build.ninja
diff -pruN 1.7.8-1/CHANGELOG.md 1.7.9-1/CHANGELOG.md
--- 1.7.8-1/CHANGELOG.md	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/CHANGELOG.md	2025-11-25 14:31:14.000000000 +0000
@@ -2,13 +2,28 @@
 
 ## master (unreleased)
 
+## v1.7.9
+
+### New features
+
+* Add support for other BSD systems (NetBSD, OpenBSD, DragonFly BSD) and Solaris
+
+## v1.7.8
+
 ### New features
 
 * Add copy file if be different
 
+### Changes
+
+* [#291](https://github.com/tboox/tbox/pull/291): Improve semaphore
+* [#292](https://github.com/tboox/tbox/pull/292): Improve process on Windows
+
 ### Bugs fixed
 
+* [#208](https://github.com/tboox/tbox/issues/208): [#290](https://github.com/tboox/tbox/pull/290): Fix stream buffer
 * [#272](https://github.com/tboox/tbox/issues/272): Fix read file stuck on windows arm64
+* [#292](https://github.com/tboox/tbox/pull/292): Fix process and path errors on Windows
 
 ## v1.7.7
 
@@ -331,13 +346,28 @@
 
 ## master (开发中)
 
+## v1.7.9
+
+### 新特性
+
+* 添加对其他 BSD 系统（NetBSD, OpenBSD, DragonFly BSD）和 Solaris 的支持
+
+## v1.7.8
+
 ### 新特性
 
 * 添加 copy if be different 支持
 
+### 改进
+
+* [#291](https://github.com/tboox/tbox/pull/291): 改进信号量
+* [#292](https://github.com/tboox/tbox/pull/292): 改进 Windows 进程处理
+
 ### Bugs 修复
 
+* [#208](https://github.com/tboox/tbox/issues/208): [#290](https://github.com/tboox/tbox/pull/290): 修复流缓冲区问题
 * [#272](https://github.com/tboox/tbox/issues/272): 修复读取文件卡住问题
+* [#292](https://github.com/tboox/tbox/pull/292): 修复 Windows 进程和路径错误
 
 ## v1.7.7
 
diff -pruN 1.7.8-1/configure 1.7.9-1/configure
--- 1.7.8-1/configure	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/configure	2025-11-25 14:31:14.000000000 +0000
@@ -29,6 +29,7 @@ xmake_sh_verbose=false
 xmake_sh_diagnosis=false
 xmake_sh_copyright="Copyright (C) 2022-present Ruki Wang, https://xmake.io."
 xmake_sh_makefile="${xmake_sh_projectdir}/Makefile"
+xmake_sh_ninjafile="${xmake_sh_projectdir}/build.ninja"
 
 #-----------------------------------------------------------------------------
 # some helper functions
@@ -101,7 +102,81 @@ string_tolower() {
 }
 
 string_replace() {
-    _ret=$(echo "$1" | sed "s/${2}/${3}/g")
+    local src="${1}"
+    local search="${2}"
+    local replace="${3}"
+    if test_z "${search}"; then
+        _ret="${src}"
+        return
+    fi
+    local rest="${src}"
+    local result=""
+    local prefix=""
+    while :; do
+        case "${rest}" in
+            *"${search}"*)
+                prefix="${rest%%"${search}"*}"
+                result="${result}${prefix}${replace}"
+                rest="${rest#*"${search}"}"
+                ;;
+            *)
+                result="${result}${rest}"
+                break
+                ;;
+        esac
+    done
+    _ret="${result}"
+}
+
+# escape value for writing into ninja files
+_ninja_escape() {
+    local rest="${1}"
+    case "${rest}" in
+        *\$*)
+            local result=""
+            while :; do
+                case "${rest}" in
+                    *\$*)
+                        result="${result}${rest%%\$*}\$\$"
+                        rest="${rest#*\$}"
+                        ;;
+                    *)
+                        result="${result}${rest}"
+                        break
+                        ;;
+                esac
+            done
+            _ret="${result}"
+            ;;
+        *)
+            _ret="${rest}"
+            ;;
+    esac
+}
+
+_shell_escape_single_quotes() {
+    local value="${1}"
+    case "${value}" in
+        *"'"*)
+            local escaped=""
+            while :; do
+                case "${value}" in
+                    *"'"*)
+                        escaped="${escaped}${value%%\'*}'\"'\"'"
+                        value="${value#*\'}"
+                        ;;
+                    *)
+                        escaped="${escaped}${value}"
+                        break
+                        ;;
+                esac
+            done
+            _ret="'${escaped}'"
+            ;;
+        *)
+            _ret="'${value}'"
+            ;;
+    esac
 }
 
 # we avoid use `cut` command, because it's slow
@@ -181,7 +256,12 @@ string_startswith() {
 string_dupch() {
     local count=${1}
     local ch=${2}
-    printf %${count}s | tr " " "${ch}"
+    local result=""
+    while [ "${count:-0}" -gt 0 ]; do
+        result="${result}${ch}"
+        count=$((count - 1))
+    done
+    printf '%s' "${result}"
 }
 
 # replace file content
@@ -237,13 +317,31 @@ _os_iorunv() {
 }
 
 # find file in the given directory
-# e.g. _os_find . xmake.sh
+# e.g. _os_find . xmake.sh [depth]
 _os_find() {
     local dir="${1}"
     local name="${2}"
     local depth="${3}"
     if test_nz "${depth}"; then
-        _ret=$(find "${dir}" -maxdepth "${depth}" -mindepth "${depth}" -type f -name "${name}" | LC_ALL=C sort)
+        # Solaris doesn't support -maxdepth, use shell to filter depth
+        if is_host "solaris"; then
+            _ret=""
+            local file=""
+            for file in $(find "${dir}" -type f -name "${name}" | LC_ALL=C sort); do
+                local relpath="${file#${dir}/}"
+                local slash_count=$(echo "${relpath}" | tr -cd '/' | wc -c | tr -d ' ')
+                if test "${slash_count}" = "$((${depth} - 1))"; then
+                    if test -z "${_ret}"; then
+                        _ret="${file}"
+                    else
+                        _ret="${_ret}
+${file}"
+                    fi
+                fi
+            done
+        else
+            _ret=$(find "${dir}" -maxdepth "${depth}" -mindepth "${depth}" -type f -name "${name}" | LC_ALL=C sort)
+        fi
     else
         _ret=$(find "${dir}" -type f -name "${name}" | LC_ALL=C sort)
     fi
@@ -601,30 +699,19 @@ _map_set() {
 # detect hosts
 os_host=`uname`
 string_tolower ${os_host}; os_host="${_ret}"
-if echo "${os_host}" | grep cygwin >/dev/null 2>&1; then
-    os_host="cygwin"
-fi
-if echo "${os_host}" | grep msys >/dev/null 2>&1; then
-    os_host="msys"
-fi
-if echo "${os_host}" | grep mingw >/dev/null 2>&1; then
-    os_host="msys"
-fi
-if echo "${os_host}" | grep darwin >/dev/null 2>&1; then
-    os_host="macosx"
-fi
-if echo "${os_host}" | grep linux >/dev/null 2>&1; then
-    os_host="linux"
-fi
-if echo "${os_host}" | grep freebsd >/dev/null 2>&1; then
-    os_host="freebsd"
-fi
-if echo "${os_host}" | grep bsd >/dev/null 2>&1; then
-    os_host="bsd"
-fi
-if echo "${os_host}" | grep Haiku >/dev/null 2>&1; then
-    os_host="haiku"
-fi
+case "${os_host}" in
+    *cygwin*) os_host="cygwin" ;;
+    *mingw*|*msys*) os_host="msys" ;;
+    *darwin*) os_host="macosx" ;;
+    *linux*) os_host="linux" ;;
+    *freebsd*) os_host="freebsd" ;;
+    *netbsd*) os_host="netbsd" ;;
+    *openbsd*) os_host="openbsd" ;;
+    *dragonfly*) os_host="dragonflybsd" ;;
+    *bsd*) os_host="bsd" ;;
+    *sunos*) os_host="solaris" ;;
+    *haiku*) os_host="haiku" ;;
+esac
 
 # determining host
 # e.g.
@@ -643,8 +730,10 @@ is_host() {
 
 # detect host architecture
 os_arch=`uname -m | tr '[A-Z]' '[a-z]'`
-if test_eq "${os_arch}" "i686"; then
+if test_eq "${os_arch}" "i686" || test_eq "${os_arch}" "i86pc"; then
     os_arch="i386"
+elif test_eq "${os_arch}" "amd64"; then
+    os_arch="x86_64"
 elif test_eq "${os_arch}" "aarch64" || test_eq "${os_arch}" "arm64"; then
     os_arch="arm64"
 elif string_contains "${os_arch}" "armv7"; then
@@ -659,7 +748,7 @@ fi
 _target_plat_default=${os_host}
 if is_host "msys"; then
     _target_plat_default="mingw"
-elif is_host "freebsd"; then
+elif is_host "freebsd" "openbsd" "dragonflybsd" "netbsd"; then
     _target_plat_default="bsd"
 elif test_nz "${EMSDK}"; then
     _target_plat_default="wasm"
@@ -691,7 +780,7 @@ _target_mode_default="release"
 _target_kind_default="static"
 
 # set the default project generator and build program
-if is_host "freebsd" "bsd"; then
+if is_host "freebsd" "netbsd" "openbsd" "dragonflybsd" "bsd" "solaris"; then
     project_generator="gmake"
     _make_program_default="gmake"
     _ninja_program_default="ninja"
@@ -838,7 +927,7 @@ _get_abstract_flag_for_gcc_clang() {
     local flag=""
     case "${itemname}" in
         defines)
-            string_replace "${value}" '"' '\\\"'; value="${_ret}"
+            string_replace "${value}" '"' '\"'; value="${_ret}"
             flag="-D${value}"
             ;;
         undefines) flag="-U${value}";;
@@ -2729,7 +2818,8 @@ _load_options_and_toolchains() {
         includes "${file}"
     else
         # include all xmake.sh files in next sub-directories
-        local files=`find ${xmake_sh_projectdir} -maxdepth 2 -mindepth 2 -name "xmake.sh"`
+        _os_find "${xmake_sh_projectdir}" "xmake.sh" "2"
+        local files="${_ret}"
         for file in ${files}; do
             includes "${file}"
         done
@@ -2841,13 +2931,19 @@ _show_version() {
 
 # --foo=yes => foo
 _parse_argument_name() {
-    _ret=$(echo "${1#*--}" | sed "s/${2-=[^=]*}$//")
-    string_replace "${_ret}" "-" "_"
+    local arg="${1#--}"
+    case "${arg}" in
+        *=*) arg="${arg%%=*}" ;;
+    esac
+    string_replace "${arg}" "-" "_"
 }
 
 # --foo=yes => yes
 _parse_argument_value() {
-    _ret=$(echo "$1" | sed "s/^${2-[^=]*=}//")
+    case "${1}" in
+        *=*) _ret="${1#*=}" ;;
+        *) _ret="" ;;
+    esac
 }
 
 # parse input arguments
@@ -4515,9 +4611,655 @@ _generate_for_gmake() {
 # generate ninja file
 #
 
+_ninja_begin() {
+    echo "generating ninja build file .."
+}
+
+_ninja_add_header() {
+    echo "# this is the build file for this project
+# it is autogenerated by the xmake.sh build system.
+# do not edit by hand.
+" > "${xmake_sh_ninjafile}"
+    echo "ninja_required_version = 1.3" >> "${xmake_sh_ninjafile}"
+    echo "builddir = ${xmake_sh_builddir}" >> "${xmake_sh_ninjafile}"
+    echo "" >> "${xmake_sh_ninjafile}"
+}
+
+_ninja_add_switches() {
+    local value=""
+    value="${_install_prefix_default}"; _ninja_escape "${value}"; value="${_ret}"
+    echo "prefix_default = ${value}" >> "${xmake_sh_ninjafile}"
+    value="${_install_bindir_default}"; _ninja_escape "${value}"; value="${_ret}"
+    echo "bindir_default = ${value}" >> "${xmake_sh_ninjafile}"
+    value="${_install_libdir_default}"; _ninja_escape "${value}"; value="${_ret}"
+    echo "libdir_default = ${value}" >> "${xmake_sh_ninjafile}"
+    value="${_install_includedir_default}"; _ninja_escape "${value}"; value="${_ret}"
+    echo "includedir_default = ${value}" >> "${xmake_sh_ninjafile}"
+    echo "" >> "${xmake_sh_ninjafile}"
+}
+
+_ninja_add_rules() {
+    echo "rule command" >> "${xmake_sh_ninjafile}"
+    echo "  command = \$command" >> "${xmake_sh_ninjafile}"
+    echo "  description = \$description" >> "${xmake_sh_ninjafile}"
+    echo "  restat = \$restat" >> "${xmake_sh_ninjafile}"
+    echo "" >> "${xmake_sh_ninjafile}"
+}
+
+_ninja_add_toolchains() {
+    _get_targets_toolkinds; local kinds="${_ret}"
+    if test_nz "${kinds}"; then
+        echo "# toolchain programs" >> "${xmake_sh_ninjafile}"
+        for kind in ${kinds}; do
+            _get_toolchain_toolset "${_target_toolchain}" "${kind}"; local program="${_ret}"
+            echo "${kind} = ${program}" >> "${xmake_sh_ninjafile}"
+        done
+        echo "" >> "${xmake_sh_ninjafile}"
+    fi
+}
+
+_ninja_add_flags() {
+    _get_targets_toolkinds; local kinds="${_ret}"
+    for target in ${_xmake_sh_targets}; do
+        for kind in ${kinds}; do
+            _get_target_flags "${target}" "${kind}"; local flags="${_ret}"
+            if test_nz "${flags}"; then
+                _get_flagname "${kind}"; local flagname="${_ret}"
+                local key="${target}_${flagname}"
+                _ninja_escape "${flags}"; flags="${_ret}"
+                echo "${key} = ${flags}" >> "${xmake_sh_ninjafile}"
+            fi
+        done
+        echo "" >> "${xmake_sh_ninjafile}"
+    done
+}
+
+_ninja_add_build_object() {
+    local target=${1}
+    local sourcefile="${2}"
+    local objectfile="${3}"
+    path_sourcekind "${sourcefile}"; local sourcekind="${_ret}"
+    _get_target_flags "${target}" "${sourcekind}"; local flags="${_ret}"
+    _toolchain_compcmd "${sourcekind}" "${objectfile}" "${sourcefile}" "${flags}"; local compcmd="${_ret}"
+    path_directory "${objectfile}"; local objectdir="${_ret}"
+    local use_shell_wrapper=false
+    local command="mkdir -p \"${objectdir}\" && ${compcmd}"
+    if is_host "msys" "cygwin" "mingw"; then
+        use_shell_wrapper=true
+    fi
+    if ${use_shell_wrapper}; then
+        _shell_escape_single_quotes "${command}"; local command_script="${_ret}"
+        command="sh -lc ${command_script}"
+    fi
+    local description="compiling.${_target_mode} ${sourcefile}"
+    _ninja_escape "${command}"; command="${_ret}"
+    _ninja_escape "${description}"; description="${_ret}"
+    echo "build ${objectfile}: command ${sourcefile}" >> "${xmake_sh_ninjafile}"
+    echo "  command = ${command}" >> "${xmake_sh_ninjafile}"
+    echo "  description = ${description}" >> "${xmake_sh_ninjafile}"
+    echo "  restat = 0" >> "${xmake_sh_ninjafile}"
+    echo "" >> "${xmake_sh_ninjafile}"
+}
+
+_ninja_add_build_objects() {
+    local target=${1}
+    _get_target_sourcefiles "${target}"; local sourcefiles="${_ret}"
+    for sourcefile in ${sourcefiles}; do
+        _get_target_objectfile "${target}" "${sourcefile}"; local objectfile="${_ret}"
+        _ninja_add_build_object "${target}" "${sourcefile}" "${objectfile}"
+    done
+}
+
+_ninja_add_build_target() {
+    local target=${1}
+    _get_targetdir "${target}"; local targetdir="${_ret}"
+    _get_target_file "${target}"; local targetfile="${_ret}"
+    _get_target_item "${target}" "deps"; local deps="${_ret}"
+    _get_target_objectfiles "${target}"; local objectfiles="${_ret}"
+
+    _get_target_item "${target}" "kind"; local targetkind="${_ret}"
+    local toolkind=""
+    case "${targetkind}" in
+        binary) toolkind="ld";;
+        static) toolkind="ar";;
+        shared) toolkind="sh";;
+        *) raise "unknown targetkind(${targetkind})!" ;;
+    esac
+    _get_target_flags "${target}" "${toolkind}"; local linkflags="${_ret}"
+    _toolchain_linkcmd "${toolkind}" "${targetfile}" "${objectfiles}" "${linkflags}"; local linkcmd="${_ret}"
+
+    local use_shell_wrapper=false
+    local command="mkdir -p \"${targetdir}\" && ${linkcmd}"
+    if is_host "msys" "cygwin" "mingw"; then
+        use_shell_wrapper=true
+    fi
+    local description="linking.${_target_mode} ${targetfile}"
+
+    if test_eq "${targetkind}" "shared"; then
+        _get_target_item "${target}" "version"; local version="${_ret}"
+        _get_target_soname "${target}"; local soname="${_ret}"
+        if test_nz "${soname}" && test_nz "${version}"; then
+            _get_target_filename "${target}"; local targetfilename="${_ret}"
+            _get_target_extension "${target}"; local extension="${_ret}"
+            local targetfile_with_version="${targetdir}/${targetfilename}.${version}"
+            if test_eq "${extension}" ".dylib"; then
+                path_basename "${targetfilename}"; local basename="${_ret}"
+                targetfile_with_version="${targetdir}/${basename}.${version}${extension}"
+            fi
+            local targetfile_with_soname="${targetdir}/${soname}"
+            path_filename "${targetfile_with_version}"; local targetfilename_with_version="${_ret}"
+            if test_nq "${soname}" "${targetfilename}" && test_nq "${soname}" "${targetfilename_with_version}"; then
+                command="${command} && cp -p ${targetfile} ${targetfile_with_version} && cd \"${targetdir}\" && ln -sf ${targetfilename_with_version} ${soname} && ln -sf ${soname} ${targetfilename}"
+            fi
+        fi
+    fi
+
+    if ${use_shell_wrapper}; then
+        _shell_escape_single_quotes "${command}"; local command_script="${_ret}"
+        command="sh -lc ${command_script}"
+    fi
+    _ninja_escape "${command}"; command="${_ret}"
+    _ninja_escape "${description}"; description="${_ret}"
+
+    local orderdeps=""
+    local dep=""
+    for dep in ${deps}; do
+        _get_target_file "${dep}"; local depfile="${_ret}"
+        if test_nz "${orderdeps}"; then
+            orderdeps="${orderdeps} ${depfile}"
+        else
+            orderdeps="${depfile}"
+        fi
+    done
+
+    if test_nz "${orderdeps}"; then
+        echo "build ${targetfile}: command ${objectfiles} | ${orderdeps}" >> "${xmake_sh_ninjafile}"
+    else
+        echo "build ${targetfile}: command ${objectfiles}" >> "${xmake_sh_ninjafile}"
+    fi
+    echo "  command = ${command}" >> "${xmake_sh_ninjafile}"
+    echo "  description = ${description}" >> "${xmake_sh_ninjafile}"
+    echo "  restat = 0" >> "${xmake_sh_ninjafile}"
+    echo "" >> "${xmake_sh_ninjafile}"
+
+    echo "build ${target}: phony ${targetfile}" >> "${xmake_sh_ninjafile}"
+    echo "" >> "${xmake_sh_ninjafile}"
+
+    _ninja_add_build_objects "${target}"
+}
+
+_ninja_add_build_targets() {
+    local target=""
+    local defaults=""
+    for target in ${_xmake_sh_targets}; do
+        if _is_target_default "${target}"; then
+            if test_nz "${defaults}"; then
+                defaults="${defaults} ${target}"
+            else
+                defaults="${target}"
+            fi
+        fi
+    done
+    if test_nz "${defaults}"; then
+        echo "build default: phony ${defaults}" >> "${xmake_sh_ninjafile}"
+        echo "" >> "${xmake_sh_ninjafile}"
+        echo "default default" >> "${xmake_sh_ninjafile}"
+    else
+        echo "default all" >> "${xmake_sh_ninjafile}"
+    fi
+    echo "build all: phony ${_xmake_sh_targets}" >> "${xmake_sh_ninjafile}"
+    echo "" >> "${xmake_sh_ninjafile}"
+    for target in ${_xmake_sh_targets}; do
+        _ninja_add_build_target "${target}"
+    done
+}
+
+_ninja_add_build() {
+    _ninja_add_build_targets
+}
+
+_ninja_add_run_target() {
+    local target=${1}
+    _get_targetdir "${target}"; local targetdir="${_ret}"
+    _get_target_file "${target}"; local targetfile="${_ret}"
+    local command=""
+    if is_host "msys" "cygwin" "mingw"; then
+        local projectdir="${xmake_sh_projectdir}"
+        local targetfile_rel="./${targetfile}"
+        local targetfile_alt="./build/${targetfile#*/}"
+        local targetbuilddir="${targetfile%/*}"
+        local targetbuilddir_rel="./${targetbuilddir}"
+        local targetbuilddir_alt="./build/${targetbuilddir#*/}"
+        local run_script="cd \"${projectdir}\" && target=\"${targetfile_rel}\"; alt=\"${targetfile_alt}\"; if [ ! -f \"\$target\" ] && [ -f \"\$alt\" ]; then target=\"\$alt\"; fi; if [ ! -f \"\$target\" ]; then echo \"[ninja run] missing ${targetfile_rel} (and fallback ${targetfile_alt})\"; ls -l \"${targetbuilddir_rel}\" || true; ls -l \"${targetbuilddir_alt}\" || true; exit 1; fi; \"\$target\""
+        _shell_escape_single_quotes "${run_script}"; local run_script_escaped="${_ret}"
+        command="sh -lc ${run_script_escaped}"
+    elif is_plat "macosx"; then
+        command="DYLD_LIBRARY_PATH=${targetdir} ${targetfile}"
+    elif is_plat "linux" "bsd"; then
+        command="LD_LIBRARY_PATH=${targetdir} ${targetfile}"
+    else
+        command="${targetfile}"
+    fi
+    local description="running.${_target_mode} ${targetfile}"
+    _ninja_escape "${command}"; command="${_ret}"
+    _ninja_escape "${description}"; description="${_ret}"
+    echo "build run.${target}: command | ${targetfile}" >> "${xmake_sh_ninjafile}"
+    echo "  command = ${command}" >> "${xmake_sh_ninjafile}"
+    echo "  description = ${description}" >> "${xmake_sh_ninjafile}"
+    echo "  restat = 0" >> "${xmake_sh_ninjafile}"
+    echo "" >> "${xmake_sh_ninjafile}"
+}
+
+_ninja_add_run_targets() {
+    local target=""
+    local runtargets=""
+    for target in ${_xmake_sh_targets}; do
+        _get_target_item "${target}" "kind"; local kind="${_ret}"
+        if test_eq "${kind}" "binary"; then
+            if _is_target_default "${target}"; then
+                if test_nz "${runtargets}"; then
+                    runtargets="${runtargets} run.${target}"
+                else
+                    runtargets="run.${target}"
+                fi
+                _ninja_add_run_target "${target}"
+            fi
+        fi
+    done
+    if test_nz "${runtargets}"; then
+        echo "build run: phony ${runtargets}" >> "${xmake_sh_ninjafile}"
+        echo "" >> "${xmake_sh_ninjafile}"
+    fi
+}
+
+_ninja_add_run() {
+    _ninja_add_run_targets
+}
+
+_ninja_add_clean_target() {
+    local target=${1}
+    _get_target_file "${target}"; local targetfile="${_ret}"
+    _get_target_objectfiles "${target}"; local objectfiles="${_ret}"
+    local removefiles="${targetfile}"
+    local objectfile=""
+    for objectfile in ${objectfiles}; do
+        removefiles="${removefiles} ${objectfile}"
+    done
+
+    _get_targetdir "${target}"; local targetdir="${_ret}"
+    _get_target_item "${target}" "kind"; local targetkind="${_ret}"
+    if test_eq "${targetkind}" "shared"; then
+        _get_target_item "${target}" "version"; local version="${_ret}"
+        _get_target_soname "${target}"; local soname="${_ret}"
+        if test_nz "${soname}" && test_nz "${version}"; then
+            _get_target_filename "${target}"; local filename="${_ret}"
+            _get_target_extension "${target}"; local extension="${_ret}"
+            local targetfile_with_version="${targetdir}/${filename}.${version}"
+            if test_eq "${extension}" ".dylib"; then
+                path_basename "${filename}"; local basename="${_ret}"
+                targetfile_with_version="${targetdir}/${basename}.${version}${extension}"
+            fi
+            local targetfile_with_soname="${targetdir}/${soname}"
+            removefiles="${removefiles} ${targetfile_with_soname} ${targetfile_with_version}"
+        fi
+    fi
+
+    local command="rm -f ${removefiles}"
+    local description="cleaning.${_target_mode} ${target}"
+    _ninja_escape "${command}"; command="${_ret}"
+    _ninja_escape "${description}"; description="${_ret}"
+    echo "build clean.${target}: command" >> "${xmake_sh_ninjafile}"
+    echo "  command = ${command}" >> "${xmake_sh_ninjafile}"
+    echo "  description = ${description}" >> "${xmake_sh_ninjafile}"
+    echo "  restat = 0" >> "${xmake_sh_ninjafile}"
+    echo "" >> "${xmake_sh_ninjafile}"
+}
+
+_ninja_add_clean_targets() {
+    local target=""
+    local cleantargets=""
+    for target in ${_xmake_sh_targets}; do
+        if _is_target_default "${target}"; then
+            if test_nz "${cleantargets}"; then
+                cleantargets="${cleantargets} clean.${target}"
+            else
+                cleantargets="clean.${target}"
+            fi
+            _ninja_add_clean_target "${target}"
+        fi
+    done
+    if test_nz "${cleantargets}"; then
+        echo "build clean: phony ${cleantargets}" >> "${xmake_sh_ninjafile}"
+        echo "" >> "${xmake_sh_ninjafile}"
+    fi
+}
+
+_ninja_add_clean() {
+    _ninja_add_clean_targets
+}
+
+_ninja_install_prepare_script() {
+    local target="${1}"
+    local ninjadir="${xmake_sh_builddir}/.ninja"
+    local scriptdir="${xmake_sh_projectdir}/${ninjadir}"
+    mkdir -p "${scriptdir}"
+    _ret="${scriptdir}/install_${target}.sh"
+    _ret2="${ninjadir}/install_${target}.sh"
+}
+
+_ninja_install_write_header() {
+    local scriptfile="${1}"
+    local escaped_prefix_default="${2}"
+    local escaped_installdir_template="${3}"
+    local escaped_projectdir="${4}"
+    local escaped_bindir_template="${5}"
+    local escaped_libdir_template="${6}"
+    local escaped_includedir_template="${7}"
+
+    cat > "${scriptfile}" <<EOF
+#!/bin/sh
+set -e
+
+prefix=${escaped_prefix_default}
+if [ -n "\${PREFIX}" ]; then
+    prefix="\${PREFIX}"
+fi
+destdir="\${DESTDIR}"
+installdir_template=${escaped_installdir_template}
+placeholder='\${prefix}'
+_rp_value=""
+_rp_replacement=""
+_rp_before=""
+_rp_after=""
+_rp_result=""
+
+replace_placeholder() {
+    _rp_value="\$1"
+    _rp_replacement="\$2"
+    while :; do
+        case "\${_rp_value}" in
+            *\${placeholder}*)
+                _rp_before="\${_rp_value%%\${placeholder}*}"
+                _rp_after="\${_rp_value#*\${placeholder}}"
+                _rp_value="\${_rp_before}\${_rp_replacement}\${_rp_after}"
+                ;;
+            *)
+                break
+                ;;
+        esac
+    done
+    _rp_result="\${_rp_value}"
+}
+
+projectdir=${escaped_projectdir}
+cd "\${projectdir}"
+
+installdir=""
+if [ -z "\${installdir_template}" ]; then
+    installdir="\${destdir}"
+    if [ -n "\${installdir}" ]; then
+        prefix_trim="\${prefix#/}"
+        installdir="\${installdir}/\${prefix_trim}"
+    else
+        installdir="\${prefix}"
+    fi
+else
+    installdir="\${installdir_template}"
+    replace_placeholder "\${installdir}" "\${prefix}"
+    installdir="\${_rp_result}"
+    if [ -n "\${destdir}" ]; then
+        case "\${installdir}" in
+            /*) ;;
+            *) installdir="\${destdir}/\${installdir}" ;;
+        esac
+    fi
+fi
+
+resolve_prefix_path() {
+    replace_placeholder "\$1" "\${installdir}"
+    printf '%s' "\${_rp_result}"
+}
+
+copy_file() {
+    local src="\$1"
+    local dst_template="\$2"
+    local dst
+    dst=\$(resolve_prefix_path "\${dst_template}")
+    echo "installing \${src} to \${dst}"
+    mkdir -p "\$(dirname "\${dst}")"
+    cp -p "\${src}" "\${dst}"
+}
+
+bindir_template=${escaped_bindir_template}
+libdir_template=${escaped_libdir_template}
+includedir_template=${escaped_includedir_template}
+EOF
+}
+
+_ninja_install_append_target_artifacts() {
+    local scriptfile="${1}"
+    local targetkind="${2}"
+    local install_for_soname="${3}"
+    local targetfile="${4}"
+    local filename="${5}"
+    local version="${6}"
+    local soname="${7}"
+    local extension="${8}"
+    local filename_basename="${9}"
+
+    if test_eq "${targetkind}" "binary"; then
+        _shell_escape_single_quotes "${targetfile}"; local escaped_targetfile="${_ret}"
+        local dest_template="${_install_bindir_default}/${filename}"
+        _shell_escape_single_quotes "${dest_template}"; local escaped_dest_template="${_ret}"
+        echo "copy_file ${escaped_targetfile} ${escaped_dest_template}" >> "${scriptfile}"
+    elif ${install_for_soname}; then
+        local version_template="${_install_libdir_default}/${filename}.${version}"
+        if test_eq "${extension}" ".dylib"; then
+            version_template="${_install_libdir_default}/${filename_basename}.${version}${extension}"
+        fi
+        _shell_escape_single_quotes "${targetfile}"; local escaped_targetfile="${_ret}"
+        _shell_escape_single_quotes "${version_template}"; local escaped_version_template="${_ret}"
+        _shell_escape_single_quotes "${soname}"; local escaped_soname="${_ret}"
+        _shell_escape_single_quotes "${filename}"; local escaped_filename="${_ret}"
+        cat >> "${scriptfile}" <<EOF
+copy_file ${escaped_targetfile} ${escaped_version_template}
+dst_version=\$(resolve_prefix_path ${escaped_version_template})
+dst_dir=\$(dirname "\${dst_version}")
+filename_version=\$(basename "\${dst_version}")
+(
+    cd "\${dst_dir}"
+    ln -sf "\${filename_version}" ${escaped_soname}
+    ln -sf ${escaped_soname} ${escaped_filename}
+)
+EOF
+    elif test_eq "${targetkind}" "static" || test_eq "${targetkind}" "shared"; then
+        _shell_escape_single_quotes "${targetfile}"; local escaped_targetfile="${_ret}"
+        local dest_template="${_install_libdir_default}/${filename}"
+        _shell_escape_single_quotes "${dest_template}"; local escaped_dest_template="${_ret}"
+        echo "copy_file ${escaped_targetfile} ${escaped_dest_template}" >> "${scriptfile}"
+    fi
+}
+
+_ninja_install_append_headerfiles() {
+    local scriptfile="${1}"
+    local headerfiles="${2}"
+    if test_nz "${headerfiles}"; then
+        local srcheaderfile=""
+        for srcheaderfile in ${headerfiles}; do
+            string_split "${srcheaderfile}" ":"
+            local srcheaderpath="${_ret}"
+            local rootdir="${_ret2}"
+            local prefixdir="${_ret3}"
+            local headername="${_ret4}"
+            if test_z "${headername}"; then
+                path_filename "${srcheaderpath}"; headername="${_ret}"
+            fi
+            local dstheaderdir_template="${_install_includedir_default}"
+            if test_nz "${prefixdir}"; then
+                dstheaderdir_template="${dstheaderdir_template}/${prefixdir}"
+            fi
+            local dstheaderfile_template=""
+            if test_nz "${rootdir}"; then
+                path_relative "${rootdir}" "${srcheaderpath}"; local subfile="${_ret}"
+                dstheaderfile_template="${dstheaderdir_template}/${subfile}"
+            else
+                dstheaderfile_template="${dstheaderdir_template}/${headername}"
+            fi
+            _shell_escape_single_quotes "${srcheaderpath}"; local escaped_src="${_ret}"
+            _shell_escape_single_quotes "${dstheaderfile_template}"; local escaped_dst="${_ret}"
+            echo "copy_file ${escaped_src} ${escaped_dst}" >> "${scriptfile}"
+        done
+    fi
+}
+
+_ninja_install_append_installfiles() {
+    local scriptfile="${1}"
+    local installdir="${2}"
+    local installfiles="${3}"
+    if test_nz "${installfiles}"; then
+        local srcinstallfile=""
+        for srcinstallfile in ${installfiles}; do
+            string_split "${srcinstallfile}" ":"
+            local srcfilepath="${_ret}"
+            local rootdir="${_ret2}"
+            local prefixdir="${_ret3}"
+            local installname="${_ret4}"
+            if test_z "${installname}"; then
+                path_filename "${srcfilepath}"; installname="${_ret}"
+            fi
+            local dst_template="${installdir}"
+            if test_z "${dst_template}"; then
+                dst_template="\${prefix}"
+            fi
+            if test_nz "${prefixdir}"; then
+                dst_template="${dst_template}/${prefixdir}"
+            fi
+            if test_nz "${rootdir}"; then
+                path_relative "${rootdir}" "${srcfilepath}"; local subfile="${_ret}"
+                dst_template="${dst_template}/${subfile}"
+            else
+                dst_template="${dst_template}/${installname}"
+            fi
+            _shell_escape_single_quotes "${srcfilepath}"; local escaped_src="${_ret}"
+            _shell_escape_single_quotes "${dst_template}"; local escaped_dst="${_ret}"
+            echo "copy_file ${escaped_src} ${escaped_dst}" >> "${scriptfile}"
+        done
+    fi
+}
+
+_ninja_add_install_target() {
+    local target=${1}
+    _get_target_file "${target}"; local targetfile="${_ret}"
+    path_filename "${targetfile}"; local filename="${_ret}"
+    _get_target_item "${target}" "installdir"; local installdir="${_ret}"
+    _get_target_item "${target}" "kind"; local targetkind="${_ret}"
+
+    local install_for_soname=false
+    local version=""
+    local soname=""
+    local extension=""
+    local filename_basename=""
+    if test_eq "${targetkind}" "shared"; then
+        _get_target_item "${target}" "version"; version="${_ret}"
+        _get_target_soname "${target}"; soname="${_ret}"
+        _get_target_extension "${target}"; extension="${_ret}"
+        path_basename "${filename}"; filename_basename="${_ret}"
+        if test_nz "${soname}" && test_nz "${version}"; then
+            local targetfilename_with_version_guess="${filename}.${version}"
+            if test_eq "${extension}" ".dylib"; then
+                targetfilename_with_version_guess="${filename_basename}.${version}${extension}"
+            fi
+            if test_nq "${soname}" "${filename}" && test_nq "${soname}" "${targetfilename_with_version_guess}"; then
+                install_for_soname=true
+            fi
+        fi
+    fi
+
+    _ninja_install_prepare_script "${target}"
+    local scriptfile="${_ret}"
+    local scriptfile_rel="${_ret2}"
+
+    _shell_escape_single_quotes "${_install_prefix_default}"; local escaped_prefix_default="${_ret}"
+    _shell_escape_single_quotes "${installdir}"; local escaped_installdir_template="${_ret}"
+    _shell_escape_single_quotes "${xmake_sh_projectdir}"; local escaped_projectdir="${_ret}"
+    _shell_escape_single_quotes "${_install_bindir_default}"; local escaped_bindir_template="${_ret}"
+    _shell_escape_single_quotes "${_install_libdir_default}"; local escaped_libdir_template="${_ret}"
+    _shell_escape_single_quotes "${_install_includedir_default}"; local escaped_includedir_template="${_ret}"
+
+    _ninja_install_write_header "${scriptfile}" \
+        "${escaped_prefix_default}" \
+        "${escaped_installdir_template}" \
+        "${escaped_projectdir}" \
+        "${escaped_bindir_template}" \
+        "${escaped_libdir_template}" \
+        "${escaped_includedir_template}"
+
+    _ninja_install_append_target_artifacts "${scriptfile}" "${targetkind}" "${install_for_soname}" \
+        "${targetfile}" "${filename}" "${version}" "${soname}" "${extension}" "${filename_basename}"
+
+    _get_target_item "${target}" "headerfiles"; local headerfiles="${_ret}"
+    _ninja_install_append_headerfiles "${scriptfile}" "${headerfiles}"
+
+    _get_target_item "${target}" "installfiles"; local installfiles="${_ret}"
+    _ninja_install_append_installfiles "${scriptfile}" "${installdir}" "${installfiles}"
+
+    echo "" >> "${scriptfile}"
+    chmod +x "${scriptfile}"
+
+    local command="sh ${scriptfile_rel}"
+    local description="installing.${_target_mode} ${target}"
+    _ninja_escape "${command}"; command="${_ret}"
+    _ninja_escape "${description}"; description="${_ret}"
+    echo "build install.${target}: command | ${targetfile}" >> "${xmake_sh_ninjafile}"
+    echo "  command = ${command}" >> "${xmake_sh_ninjafile}"
+    echo "  description = ${description}" >> "${xmake_sh_ninjafile}"
+    echo "  restat = 0" >> "${xmake_sh_ninjafile}"
+    echo "" >> "${xmake_sh_ninjafile}"
+}
+
+_ninja_add_install_targets() {
+    local target=""
+    local installtargets=""
+    for target in ${_xmake_sh_targets}; do
+        if _is_target_default "${target}"; then
+            if test_nz "${installtargets}"; then
+                installtargets="${installtargets} install.${target}"
+            else
+                installtargets="install.${target}"
+            fi
+            _ninja_add_install_target "${target}"
+        fi
+    done
+    if test_nz "${installtargets}"; then
+        echo "build install: phony ${installtargets}" >> "${xmake_sh_ninjafile}"
+        echo "" >> "${xmake_sh_ninjafile}"
+    fi
+}
+
+_ninja_add_install() {
+    _ninja_add_install_targets
+}
+
+_ninja_done() {
+    echo "ninja build file is generated!"
+    if "${xmake_sh_diagnosis}"; then
+        cat "${xmake_sh_ninjafile}"
+    fi
+}
+
 # generate build file for ninja
 _generate_for_ninja() {
-    raise "Ninja generator has been not supported!"
+    _ninja_begin
+    _ninja_add_header
+    _ninja_add_switches
+    _ninja_add_toolchains
+    _ninja_add_flags
+    _ninja_add_rules
+    _ninja_add_build
+    _ninja_add_clean
+    _ninja_add_install
+    _ninja_add_run
+    _ninja_done
 }
 
 #-----------------------------------------------------------------------------
diff -pruN 1.7.8-1/debian/changelog 1.7.9-1/debian/changelog
--- 1.7.8-1/debian/changelog	2025-10-25 11:24:33.000000000 +0000
+++ 1.7.9-1/debian/changelog	2025-12-02 13:21:08.000000000 +0000
@@ -1,3 +1,11 @@
+tbox (1.7.9-1) unstable; urgency=medium
+
+  * New upstream version
+  * d/control: Remove redundant Rules-Requires-Root field
+  * d/watch: Update to version 5
+
+ -- Lance Lin <lq27267@gmail.com>  Tue, 02 Dec 2025 20:21:08 +0700
+
 tbox (1.7.8-1) unstable; urgency=medium
 
   * New upstream version
diff -pruN 1.7.8-1/debian/control 1.7.9-1/debian/control
--- 1.7.8-1/debian/control	2025-10-25 11:10:38.000000000 +0000
+++ 1.7.9-1/debian/control	2025-12-02 12:30:31.000000000 +0000
@@ -5,7 +5,6 @@ Maintainer: Yangfl <mmyangfl@gmail.com>
 Uploaders: Lance Lin <lq27267@gmail.com>
 Build-Depends:
  debhelper-compat (= 13),
-Rules-Requires-Root: no
 Standards-Version: 4.7.2
 Homepage: https://github.com/tboox/tbox
 Vcs-Git: https://salsa.debian.org/xmake-team/tbox.git
diff -pruN 1.7.8-1/debian/watch 1.7.9-1/debian/watch
--- 1.7.8-1/debian/watch	2025-10-25 11:09:36.000000000 +0000
+++ 1.7.9-1/debian/watch	2025-12-02 12:31:53.000000000 +0000
@@ -1,3 +1,4 @@
-version=4
-opts="filenamemangle=s%.+\/v?(\d\S+)\.tar\.gz%tbox-$1.tar.gz%" \
-   https://github.com/tboox/tbox/tags .*/v?(\d\S+)\.tar\.gz
+Version: 5
+Template: GitHub
+Owner: tboox
+Project: tbox
diff -pruN 1.7.8-1/src/demo/platform/process.c 1.7.9-1/src/demo/platform/process.c
--- 1.7.8-1/src/demo/platform/process.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/demo/platform/process.c	2025-11-25 14:31:14.000000000 +0000
@@ -130,6 +130,265 @@ static tb_void_t tb_demo_process_test_ex
     tb_getchar();
 }
 
+/* test: redirect stdout only, stderr should still output to terminal
+ * @see https://github.com/xmake-io/xmake/issues/3138
+ * 
+ * Verification: 
+ * - stdout should be captured in file (we read and display it)
+ * - stderr should output to terminal (visible as console output before/after this trace)
+ */
+static tb_void_t tb_demo_process_test_redirect_stdout_only(tb_char_t const* test_cmd)
+{
+    tb_trace_i("test: redirect stdout only, stderr should still output to terminal");
+    tb_trace_i("verification: stdout goes to file (read below), stderr should appear in console output");
+    tb_trace_i("NOTE: Look for 'This goes to stderr' message in your console/terminal output (may appear between these log lines)");
+    tb_trace_i("===== STDOUT REDIRECTED - STDERR SHOULD APPEAR BELOW =====");
+
+    // create temp file for stdout
+    tb_char_t tmpdir[TB_PATH_MAXN];
+    tb_char_t stdout_path[TB_PATH_MAXN];
+    tb_char_t stderr_path[TB_PATH_MAXN];
+    if (tb_directory_temporary(tmpdir, sizeof(tmpdir)))
+    {
+        tb_snprintf(stdout_path, sizeof(stdout_path), "%s%ctest_stdout.txt", tmpdir, TB_PATH_SEPARATOR);
+        tb_snprintf(stderr_path, sizeof(stderr_path), "%s%ctest_stderr.txt", tmpdir, TB_PATH_SEPARATOR);
+        
+        // create a second test: also redirect stderr to a file to verify it's actually working
+        // First test: only redirect stdout (the fix should make stderr go to terminal)
+        tb_process_attr_t attr = {0};
+        attr.out.path = stdout_path;
+        attr.outtype = TB_PROCESS_REDIRECT_TYPE_FILEPATH;
+
+        // use PowerShell to output to stderr - this is the most reliable method
+        // PowerShell's [Console]::Error.WriteLine directly writes to stderr stream
+        tb_char_t* argv[] = {"powershell", "-Command", "[Console]::Out.WriteLine('This goes to stdout'); [Console]::Error.WriteLine('This goes to stderr')", tb_null};
+        tb_process_ref_t process = tb_process_init("powershell", (tb_char_t const**)argv, &attr);
+        if (process)
+        {
+            // wait process
+            tb_long_t status = 0;
+            tb_process_wait(process, &status, -1);
+            tb_trace_i("process exited with status: %ld", status);
+
+            // read stdout from file
+            tb_file_ref_t file = tb_file_init(stdout_path, TB_FILE_MODE_RO);
+            if (file)
+            {
+                tb_byte_t data[8192];
+                tb_long_t size = tb_file_read(file, data, sizeof(data) - 1);
+                if (size > 0)
+                {
+                    data[size] = '\0';
+                    tb_trace_i("stdout from file: %s", (tb_char_t const*)data);
+                }
+                tb_file_exit(file);
+            }
+
+            // exit process
+            tb_process_exit(process);
+        }
+
+        // Second test: explicitly redirect stderr to a file to verify stderr works
+        // This proves stderr can be written to, which means the handle is correct
+        tb_trace_i("Verification: Now testing if stderr can be explicitly captured...");
+        tb_process_attr_t attr2 = {0};
+        attr2.out.path = stdout_path;
+        attr2.outtype = TB_PROCESS_REDIRECT_TYPE_FILEPATH;
+        attr2.err.path = stderr_path;
+        attr2.errtype = TB_PROCESS_REDIRECT_TYPE_FILEPATH;
+        
+        // use same PowerShell command for verification
+        tb_process_ref_t process2 = tb_process_init("powershell", (tb_char_t const**)argv, &attr2);
+        if (process2)
+        {
+            tb_long_t status2 = 0;
+            tb_process_wait(process2, &status2, -1);
+            
+            // read stderr from file - if we can read it, stderr works
+            tb_file_ref_t stderr_file = tb_file_init(stderr_path, TB_FILE_MODE_RO);
+            if (stderr_file)
+            {
+                tb_byte_t stderr_data[8192];
+                tb_long_t stderr_size = tb_file_read(stderr_file, stderr_data, sizeof(stderr_data) - 1);
+                if (stderr_size > 0)
+                {
+                    stderr_data[stderr_size] = '\0';
+                    tb_trace_i("stderr verification (explicit redirect): %s", (tb_char_t const*)stderr_data);
+                    tb_trace_i("SUCCESS: stderr can be written to, which proves handles are working correctly!");
+                }
+                else
+                {
+                    tb_trace_i("WARNING: stderr file is empty - stderr may not be working");
+                }
+                tb_file_exit(stderr_file);
+            }
+            tb_process_exit(process2);
+        }
+
+        // remove temp files
+        tb_file_remove(stdout_path);
+        tb_file_remove(stderr_path);
+    }
+    tb_trace_i("===== Check console above for 'This goes to stderr' message =====");
+}
+
+/* test: redirect stdin only, stdout and stderr should still output to terminal
+ * @see https://github.com/xmake-io/xmake/issues/3138
+ * 
+ * Verification:
+ * - stdin is redirected from file (process should be able to use it)
+ * - stdout should output to terminal (visible as console output)
+ * - stderr should output to terminal (visible as console output)
+ */
+static tb_void_t tb_demo_process_test_redirect_stdin_only(tb_char_t const* test_cmd)
+{
+    tb_trace_i("test: redirect stdin only, stdout and stderr should still output to terminal");
+    tb_trace_i("verification: stdin from file, stdout and stderr should appear in console output");
+    tb_trace_i("NOTE: Look for 'This goes to stdout' and 'This goes to stderr' messages (may appear between log lines)");
+    tb_trace_i("===== STDIN REDIRECTED - STDOUT/STDERR SHOULD APPEAR BELOW =====");
+
+    // create temp file for stdin (with some test content)
+    tb_char_t tmpdir[TB_PATH_MAXN];
+    tb_char_t stdin_path[TB_PATH_MAXN];
+    if (tb_directory_temporary(tmpdir, sizeof(tmpdir)))
+    {
+        tb_snprintf(stdin_path, sizeof(stdin_path), "%s%ctest_stdin.txt", tmpdir, TB_PATH_SEPARATOR);
+        // write test content to stdin file
+        tb_file_ref_t file = tb_file_init(stdin_path, TB_FILE_MODE_RW | TB_FILE_MODE_CREAT | TB_FILE_MODE_TRUNC);
+        if (file)
+        {
+            tb_char_t const* content = "test input\n";
+            tb_file_writ(file, (tb_byte_t const*)content, tb_strlen(content));
+            tb_file_exit(file);
+        }
+
+        // init process with stdin redirected from file
+        tb_process_attr_t attr = {0};
+        attr.in.path = stdin_path;
+        attr.intype = TB_PROCESS_REDIRECT_TYPE_FILEPATH;
+
+        // use PowerShell to output to stdout and stderr
+        // stdin is redirected but stdout/stderr should output to terminal
+        tb_char_t* argv[] = {"powershell", "-Command", "[Console]::Out.WriteLine('This goes to stdout'); [Console]::Error.WriteLine('This goes to stderr')", tb_null};
+        tb_process_ref_t process = tb_process_init("powershell", (tb_char_t const**)argv, &attr);
+        if (process)
+        {
+            // wait process
+            tb_long_t status = 0;
+            tb_process_wait(process, &status, -1);
+            tb_trace_i("process exited with status: %ld", status);
+
+            // exit process
+            tb_process_exit(process);
+        }
+
+        // remove temp file
+        tb_file_remove(stdin_path);
+    }
+    tb_trace_i("===== IF YOU SAW 'This goes to stdout/stderr' ABOVE, THE FIX WORKS! =====");
+}
+
+/* test: redirect stdout to pipe only, stderr should still output to terminal
+ * @see https://github.com/xmake-io/xmake/issues/3138
+ * 
+ * Verification:
+ * - stdout should be captured in pipe (we read and display it)
+ * - stderr should output to terminal (visible as console output)
+ */
+static tb_void_t tb_demo_process_test_redirect_stdout_pipe_only(tb_char_t const* test_cmd)
+{
+    tb_trace_i("test: redirect stdout to pipe only, stderr should still output to terminal");
+    tb_trace_i("verification: stdout goes to pipe (read below), stderr should appear in console output");
+    tb_trace_i("NOTE: Look for 'This goes to stderr' message (may appear between log lines)");
+    tb_trace_i("===== STDOUT TO PIPE - STDERR SHOULD APPEAR BELOW =====");
+
+    // init pipe files
+    tb_pipe_file_ref_t file[2] = {0};
+    if (tb_pipe_file_init_pair(file, tb_null, 0))
+    {
+        // init process with stdout redirected to pipe
+        tb_process_attr_t attr = {0};
+        attr.out.pipe = file[1];
+        attr.outtype = TB_PROCESS_REDIRECT_TYPE_PIPE;
+
+        // use PowerShell to output to stdout and stderr
+        tb_char_t* argv[] = {"powershell", "-Command", "[Console]::Out.WriteLine('This goes to stdout'); [Console]::Error.WriteLine('This goes to stderr')", tb_null};
+        tb_process_ref_t process = tb_process_init("powershell", (tb_char_t const**)argv, &attr);
+        if (process)
+        {
+            // read stdout from pipe
+            tb_size_t read = 0;
+            tb_byte_t data[8192];
+            tb_size_t size = sizeof(data);
+            tb_bool_t wait = tb_false;
+            while (read < size)
+            {
+                tb_long_t real = tb_pipe_file_read(file[0], data + read, size - read);
+                if (real > 0)
+                {
+                    read += real;
+                    wait = tb_false;
+                }
+                else if (!real && !wait)
+                {
+                    // wait pipe
+                    tb_long_t ok = tb_pipe_file_wait(file[0], TB_PIPE_EVENT_READ, 1000);
+                    if (ok <= 0) break;
+                    wait = tb_true;
+                }
+                else break;
+            }
+
+            // dump stdout data from pipe
+            if (read)
+            {
+                data[read] = '\0';
+                tb_trace_i("stdout from pipe: %s", (tb_char_t const*)data);
+            }
+
+            // wait process
+            tb_long_t status = 0;
+            tb_process_wait(process, &status, -1);
+            // Note: exit status 1 may be normal when using >&2 redirection in cmd
+            // The important verification is that stdout was captured correctly
+            tb_trace_i("process exited with status: %ld (note: non-zero may be normal)", status);
+
+            // exit process
+            tb_process_exit(process);
+        }
+
+        // exit pipe files
+        tb_pipe_file_exit(file[0]);
+        tb_pipe_file_exit(file[1]);
+    }
+    tb_trace_i("===== IF YOU SAW 'This goes to stderr' ABOVE, THE FIX WORKS! =====");
+}
+
+/* test: all redirect scenarios
+ */
+static tb_void_t tb_demo_process_test_redirect_all(tb_char_t const* test_cmd)
+{
+    tb_trace_i("test: all redirect scenarios");
+    tb_trace_i("");
+
+    // test 1: stdout only
+    tb_trace_i("=== Test 1: Redirect stdout only ===");
+    tb_demo_process_test_redirect_stdout_only(test_cmd);
+    tb_trace_i("");
+
+    // test 2: stdin only
+    tb_trace_i("=== Test 2: Redirect stdin only ===");
+    tb_demo_process_test_redirect_stdin_only(test_cmd);
+    tb_trace_i("");
+
+    // test 3: stdout pipe only
+    tb_trace_i("=== Test 3: Redirect stdout to pipe only ===");
+    tb_demo_process_test_redirect_stdout_pipe_only(test_cmd);
+    tb_trace_i("");
+
+    tb_trace_i("all redirect tests completed");
+}
+
 /* //////////////////////////////////////////////////////////////////////////////////////
  * main
  */
@@ -153,7 +412,7 @@ tb_int_t tb_demo_platform_process_main(t
     tb_used(tb_demo_process_test_waitlist);
 #endif
 
-#if 1
+#if 0
     // we can run `xxx.bat` or `xxx.sh` shell command to test it
     // @see https://github.com/xmake-io/xmake/issues/719
     tb_demo_process_test_exit(argv, tb_false);
@@ -161,5 +420,19 @@ tb_int_t tb_demo_platform_process_main(t
 #else
     tb_used(tb_demo_process_test_exit);
 #endif
+
+#if 1
+    // test: Windows process redirect scenarios
+    // @see https://github.com/xmake-io/xmake/issues/3138
+    // verify that when only stdout is redirected, stderr still outputs to terminal
+    tb_char_t const* test_cmd = argv[1];
+    if (!test_cmd) test_cmd = "cmd";
+    tb_demo_process_test_redirect_all(test_cmd);
+#else
+    tb_used(tb_demo_process_test_redirect_all);
+    tb_used(tb_demo_process_test_redirect_stdout_only);
+    tb_used(tb_demo_process_test_redirect_stdin_only);
+    tb_used(tb_demo_process_test_redirect_stdout_pipe_only);
+#endif
     return 0;
 }
diff -pruN 1.7.8-1/src/tbox/memory/buffer.c 1.7.9-1/src/tbox/memory/buffer.c
--- 1.7.8-1/src/tbox/memory/buffer.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/memory/buffer.c	2025-11-25 14:31:14.000000000 +0000
@@ -42,63 +42,43 @@
  */
 tb_bool_t tb_buffer_init(tb_buffer_ref_t buffer)
 {
-    // check
     tb_assert_and_check_return_val(buffer, tb_false);
 
-    // init
     buffer->data = buffer->buff;
     buffer->size = 0;
     buffer->maxn = sizeof(buffer->buff);
-
-    // ok
     return tb_true;
 }
 tb_void_t tb_buffer_exit(tb_buffer_ref_t buffer)
 {
-    // check
     tb_assert_and_check_return(buffer);
 
-    // clear it
     tb_buffer_clear(buffer);
 
-    // exit data
     if (buffer->data && buffer->data != buffer->buff) tb_free(buffer->data);
     buffer->data = buffer->buff;
 
-    // exit size
     buffer->size = 0;
     buffer->maxn = sizeof(buffer->buff);
 }
 tb_byte_t* tb_buffer_data(tb_buffer_ref_t buffer)
 {
-    // check
     tb_assert_and_check_return_val(buffer, tb_null);
-
-    // the buffer data
     return buffer->data;
 }
 tb_size_t tb_buffer_size(tb_buffer_ref_t buffer)
 {
-    // check
     tb_assert_and_check_return_val(buffer, 0);
-
-    // the buffer size
     return buffer->size;
 }
 tb_size_t tb_buffer_maxn(tb_buffer_ref_t buffer)
 {
-    // check
     tb_assert_and_check_return_val(buffer, 0);
-
-    // the buffer maxn
     return buffer->maxn;
 }
 tb_void_t tb_buffer_clear(tb_buffer_ref_t buffer)
 {
-    // check
     tb_assert_and_check_return(buffer);
-
-    // clear it
     buffer->size = 0;
 }
 tb_byte_t* tb_buffer_resize(tb_buffer_ref_t buffer, tb_size_t size)
@@ -106,7 +86,6 @@ tb_byte_t* tb_buffer_resize(tb_buffer_re
     // check
     tb_assert_and_check_return_val(buffer && size, tb_null);
 
-    // done
     tb_bool_t   ok = tb_false;
     tb_byte_t*  buff_data = buffer->data;
     tb_size_t   buff_size = buffer->size;
@@ -119,34 +98,26 @@ tb_byte_t* tb_buffer_resize(tb_buffer_re
         // using static buffer?
         if (buff_data == buffer->buff)
         {
-            // grow?
             if (size > buff_maxn)
             {
-                // grow maxn
-                buff_maxn = tb_align8(size + TB_BUFFER_GROW_SIZE);
+                buff_maxn = tb_align_pow2(size + TB_BUFFER_GROW_SIZE);
                 tb_assert_and_check_break(size <= buff_maxn);
 
-                // grow data
                 buff_data = tb_malloc_bytes(buff_maxn);
                 tb_assert_and_check_break(buff_data);
 
-                // copy data
                 tb_memcpy(buff_data, buffer->buff, buff_size);
             }
 
-            // update the size
             buff_size = size;
         }
         else
         {
-            // grow?
             if (size > buff_maxn)
             {
-                // grow maxn
-                buff_maxn = tb_align8(size + TB_BUFFER_GROW_SIZE);
+                buff_maxn = tb_align_pow2(size + TB_BUFFER_GROW_SIZE);
                 tb_assert_and_check_break(size <= buff_maxn);
 
-                // grow data
                 buff_data = (tb_byte_t*)tb_ralloc(buff_data, buff_maxn);
                 tb_assert_and_check_break(buff_data);
             }
@@ -154,38 +125,24 @@ tb_byte_t* tb_buffer_resize(tb_buffer_re
             // decrease to the static buffer
             else if (size <= sizeof(buffer->buff))
             {
-                // update the maxn
                 buff_maxn = sizeof(buffer->buff);
-
-                // copy data
                 tb_memcpy(buffer->buff, buff_data, size);
-
-                // free data
                 tb_free(buff_data);
-
-                // using the static buffer
                 buff_data = buffer->buff;
             }
 #endif
-
-            // update the size
             buff_size = size;
         }
 
-        // update the buffer
         buffer->data = buff_data;
         buffer->size = buff_size;
         buffer->maxn = buff_maxn;
-
-        // ok
         ok = tb_true;
 
     } while (0);
 
-    // trace
     tb_assertf(ok, "resize buffer failed: %lu => %lu", buff_size, size);
 
-    // ok
     return ok? (tb_byte_t*)buffer->data : tb_null;
 }
 tb_byte_t* tb_buffer_memset(tb_buffer_ref_t buffer, tb_byte_t b)
@@ -206,8 +163,6 @@ tb_byte_t* tb_buffer_memnsetp(tb_buffer_
 {
     // check
     tb_assert_and_check_return_val(buffer, tb_null);
-
-    // check
     tb_check_return_val(n, tb_buffer_data(buffer));
 
     // resize
@@ -216,8 +171,6 @@ tb_byte_t* tb_buffer_memnsetp(tb_buffer_
 
     // memset
     tb_memset(d + p, b, n);
-
-    // ok?
     return d;
 }
 tb_byte_t* tb_buffer_memcpy(tb_buffer_ref_t buffer, tb_buffer_ref_t b)
@@ -236,8 +189,6 @@ tb_byte_t* tb_buffer_memncpyp(tb_buffer_
 {
     // check
     tb_assert_and_check_return_val(buffer && b, tb_null);
-
-    // check
     tb_check_return_val(n, tb_buffer_data(buffer));
 
     // resize
@@ -246,8 +197,6 @@ tb_byte_t* tb_buffer_memncpyp(tb_buffer_
 
     // copy it
     tb_memcpy(d + p, b, n);
-
-    // ok
     return d;
 }
 tb_byte_t* tb_buffer_memmov(tb_buffer_ref_t buffer, tb_size_t b)
@@ -285,7 +234,6 @@ tb_byte_t* tb_buffer_memnmovp(tb_buffer_
     tb_byte_t* d = tb_buffer_resize(buffer, p + n);
     tb_assert_and_check_return_val(d, tb_null);
 
-    // memmov
     tb_memmov(d + p, d + b, n);
     return d;
 }
@@ -297,8 +245,6 @@ tb_byte_t* tb_buffer_memncat(tb_buffer_r
 {
     // check
     tb_assert_and_check_return_val(buffer && b, tb_null);
-
-    // check
     tb_check_return_val(n, tb_buffer_data(buffer));
 
     // is null?
@@ -311,8 +257,6 @@ tb_byte_t* tb_buffer_memncat(tb_buffer_r
 
     // memcat
     tb_memcpy(d + p, b, n);
-
-    // ok?
     return d;
 }
 
diff -pruN 1.7.8-1/src/tbox/object/impl/reader/json.c 1.7.9-1/src/tbox/object/impl/reader/json.c
--- 1.7.8-1/src/tbox/object/impl/reader/json.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/object/impl/reader/json.c	2025-11-25 14:31:14.000000000 +0000
@@ -507,14 +507,16 @@ static tb_size_t tb_oc_json_reader_probe
     // check
     tb_assert_and_check_return_val(stream, 0);
 
-    // need it
+    // need it, try to get up to 5 bytes (but accept less for small JSON like "{}")
     tb_byte_t*  p = tb_null;
-    if (!tb_stream_need(stream, &p, 5)) return 0;
+    tb_size_t   left = tb_stream_left(stream);
+    tb_size_t   need = tb_min(left, 5);
+    if (!need || !tb_stream_need(stream, &p, need)) return 0;
     tb_assert_and_check_return_val(p, 0);
 
     // probe it
     tb_size_t   s = 10;
-    tb_byte_t*  e = p + 5;
+    tb_byte_t*  e = p + need;
     for (; p < e && *p; p++)
     {
         if (*p == '{' || *p == '[')
diff -pruN 1.7.8-1/src/tbox/platform/mach/semaphore.c 1.7.9-1/src/tbox/platform/mach/semaphore.c
--- 1.7.8-1/src/tbox/platform/mach/semaphore.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/platform/mach/semaphore.c	2025-11-25 14:31:14.000000000 +0000
@@ -23,6 +23,7 @@
  * includes
  */
 #include "prefix.h"
+#include "../time.h"
 #include <time.h>
 #include <errno.h>
 #include <mach/semaphore.h>
@@ -134,31 +135,62 @@ tb_long_t tb_semaphore_wait(tb_semaphore
     tb_semaphore_impl_t* impl = (tb_semaphore_impl_t*)semaphore;
     tb_assert_and_check_return_val(semaphore, -1);
 
-    // init timespec
-    mach_timespec_t spec = {0};
+    // the deadline (milliseconds)
+    tb_hong_t deadline = 0;
     if (timeout > 0)
+        deadline = tb_mclock() + timeout;
+
+    kern_return_t result = KERN_SUCCESS;
+
+    while (tb_true)
     {
-        spec.tv_sec += timeout / 1000;
-        spec.tv_nsec += (timeout % 1000) * 1000000;
-    }
-    else if (timeout < 0) spec.tv_sec += 12 * 30 * 24 * 3600; // infinity: one year
+        if (timeout < 0)
+        {
+            // infinite wait
+            result = semaphore_wait(impl->semaphore);
+        }
+        else
+        {
+            // compute remaining time (milliseconds)
+            tb_hong_t remain = timeout;
+            if (timeout > 0)
+            {
+                remain = deadline - tb_mclock();
+                if (remain <= 0)
+                    return 0;
+            }
+
+            if (remain < 0)
+                remain = 0;
+
+            mach_timespec_t spec;
+            spec.tv_sec = (unsigned int)tb_min(remain / 1000, (tb_hong_t)TB_MAXU32);
+            spec.tv_nsec = (unsigned int)(((remain % 1000) < 0? 0 : (remain % 1000)) * 1000000);
+
+            result = semaphore_timedwait(impl->semaphore, spec);
+        }
 
-    // wait
-    tb_long_t ok = semaphore_timedwait(impl->semaphore, spec);
+        if (result == KERN_SUCCESS)
+            break;
 
-    // timeout or interrupted?
-    tb_check_return_val(ok != KERN_OPERATION_TIMED_OUT && ok != KERN_ABORTED, 0);
+        if (result == KERN_OPERATION_TIMED_OUT)
+            return 0;
 
-    // ok?
-    tb_check_return_val(ok == KERN_SUCCESS, -1);
+        if (result == KERN_ABORTED)
+        {
+            // The wait was interrupted (e.g. by thread cancellation). Retry until timeout expires.
+            if (!timeout)
+                return 0;
+            continue;
+        }
+
+        return -1;
+    }
 
     // check value
     tb_assert_and_check_return_val((tb_long_t)tb_atomic32_get(&impl->value) > 0, -1);
 
-    // value--
     tb_atomic32_fetch_and_sub(&impl->value, 1);
-
-    // ok
     return 1;
 }
 
diff -pruN 1.7.8-1/src/tbox/platform/path.c 1.7.9-1/src/tbox/platform/path.c
--- 1.7.8-1/src/tbox/platform/path.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/platform/path.c	2025-11-25 14:31:14.000000000 +0000
@@ -34,24 +34,6 @@
 #include "../libc/libc.h"
 
 /* //////////////////////////////////////////////////////////////////////////////////////
- * macros
- */
-
-// the path separator
-#if defined(TB_CONFIG_OS_WINDOWS) && !defined(TB_COMPILER_LIKE_UNIX)
-#   define TB_PATH_SEPARATOR            '\\'
-#else
-#   define TB_PATH_SEPARATOR            '/'
-#endif
-
-// is path separator?
-#if defined(TB_CONFIG_OS_WINDOWS) && !defined(TB_COMPILER_LIKE_UNIX)
-#   define tb_path_is_sep(c)      ('/' == (c) || '\\' == (c))
-#else
-#   define tb_path_is_sep(c)      ('/' == (c))
-#endif
-
-/* //////////////////////////////////////////////////////////////////////////////////////
  * implementation
  */
 #ifndef TB_CONFIG_MICRO_ENABLE
diff -pruN 1.7.8-1/src/tbox/platform/path.h 1.7.9-1/src/tbox/platform/path.h
--- 1.7.8-1/src/tbox/platform/path.h	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/platform/path.h	2025-11-25 14:31:14.000000000 +0000
@@ -39,6 +39,20 @@ __tb_extern_c_enter__
 // the path maximum
 #define TB_PATH_MAXN        (4096)
 
+// the path separator
+#if defined(TB_CONFIG_OS_WINDOWS) && !defined(TB_COMPILER_LIKE_UNIX)
+#   define TB_PATH_SEPARATOR            '\\'
+#else
+#   define TB_PATH_SEPARATOR            '/'
+#endif
+
+// is path separator?
+#if defined(TB_CONFIG_OS_WINDOWS) && !defined(TB_COMPILER_LIKE_UNIX)
+#   define tb_path_is_sep(c)      ('/' == (c) || '\\' == (c))
+#else
+#   define tb_path_is_sep(c)      ('/' == (c))
+#endif
+
 /* //////////////////////////////////////////////////////////////////////////////////////
  * interfaces
  */
diff -pruN 1.7.8-1/src/tbox/platform/posix/file.c 1.7.9-1/src/tbox/platform/posix/file.c
--- 1.7.8-1/src/tbox/platform/posix/file.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/platform/posix/file.c	2025-11-25 14:31:14.000000000 +0000
@@ -34,6 +34,10 @@
 #include <sys/uio.h>
 #include <unistd.h>
 #include <errno.h>
+#include <utime.h>
+#ifdef TB_CONFIG_POSIX_HAVE_UTIMENSAT
+#   include <sys/stat.h>
+#endif
 #ifdef TB_CONFIG_POSIX_HAVE_COPYFILE
 #   include <copyfile.h>
 #endif
@@ -690,18 +694,38 @@ tb_bool_t tb_file_touch(tb_char_t const*
 
     // file exists?
     tb_bool_t ok = tb_false;
-    struct timespec ts[2];
-    tb_memset(ts, 0, sizeof(ts));
     if (!access(path, F_OK))
     {
         if (atime > 0 || mtime > 0)
         {
 #ifdef TB_CONFIG_POSIX_HAVE_UTIMENSAT
+            struct timespec ts[2];
+            tb_memset(ts, 0, sizeof(ts));
             if (atime > 0) ts[0].tv_sec = atime;
             else ts[0].tv_nsec = UTIME_OMIT;
             if (mtime > 0) ts[1].tv_sec = mtime;
             else ts[1].tv_nsec = UTIME_OMIT;
             ok = !utimensat(AT_FDCWD, path, ts, 0);
+#else
+            // Fallback to utimes for systems without utimensat (e.g. Solaris)
+            struct utimbuf ut;
+            if (atime > 0) ut.actime = atime;
+            else
+            {
+                // Get current access time if not specified
+                struct stat st;
+                if (!stat(path, &st)) ut.actime = st.st_atime;
+                else ut.actime = 0;
+            }
+            if (mtime > 0) ut.modtime = mtime;
+            else
+            {
+                // Get current modify time if not specified
+                struct stat st;
+                if (!stat(path, &st)) ut.modtime = st.st_mtime;
+                else ut.modtime = 0;
+            }
+            ok = !utime(path, &ut);
 #endif
         }
         else ok = tb_true;
@@ -715,11 +739,24 @@ tb_bool_t tb_file_touch(tb_char_t const*
             if (atime > 0 || mtime > 0)
             {
 #ifdef TB_CONFIG_POSIX_HAVE_FUTIMENS
+                struct timespec ts[2];
+                tb_memset(ts, 0, sizeof(ts));
                 if (atime > 0) ts[0].tv_sec = atime;
                 else ts[0].tv_nsec = UTIME_OMIT;
                 if (mtime > 0) ts[1].tv_sec = mtime;
                 else ts[1].tv_nsec = UTIME_OMIT;
                 ok = !futimens(tb_file2fd(file), ts);
+#else
+                // Fallback to utime for systems without futimens (e.g. Solaris)
+                // Need to close file first before using utime
+                tb_file_exit(file);
+                struct utimbuf ut;
+                if (atime > 0) ut.actime = atime;
+                else ut.actime = 0;
+                if (mtime > 0) ut.modtime = mtime;
+                else ut.modtime = 0;
+                ok = !utime(path, &ut);
+                return ok;
 #endif
             }
             else ok = tb_true;
diff -pruN 1.7.8-1/src/tbox/platform/posix/sched_affinity.c 1.7.9-1/src/tbox/platform/posix/sched_affinity.c
--- 1.7.8-1/src/tbox/platform/posix/sched_affinity.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/platform/posix/sched_affinity.c	2025-11-25 14:31:14.000000000 +0000
@@ -27,6 +27,9 @@
 #include <string.h>
 #include <sched.h>
 #include <sys/types.h>
+#ifdef __NetBSD__
+#include <sched.h>
+#endif
 
 /* //////////////////////////////////////////////////////////////////////////////////////
  * implementation
@@ -36,7 +39,23 @@ tb_bool_t tb_sched_setaffinity(tb_size_t
     // check
     tb_assert_and_check_return_val(cpuset, tb_false);
 
-    // set cpu affinity
+#ifdef __NetBSD__
+    // NetBSD uses cpuset_t API for process affinity
+    cpuset_t *cpu_set = cpuset_create();
+    if (!cpu_set)
+        return tb_false;
+
+    tb_int_t i;
+    for (i = 0; i < TB_CPUSET_SIZE; i++)
+    {
+        if (TB_CPUSET_ISSET(i, cpuset))
+            cpuset_set(i, cpu_set);
+    }
+    tb_bool_t ok = cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, (pid_t)pid, cpuset_size(cpu_set), cpu_set) == 0;
+    cpuset_destroy(cpu_set);
+    return ok;
+#else
+    // Linux uses cpu_set_t API
     tb_int_t i;
     cpu_set_t cpu_set;
     CPU_ZERO(&cpu_set);
@@ -46,13 +65,37 @@ tb_bool_t tb_sched_setaffinity(tb_size_t
             CPU_SET(i, &cpu_set);
     }
     return sched_setaffinity((pid_t)pid, sizeof(cpu_set_t), &cpu_set) == 0;
+#endif
 }
 tb_bool_t tb_sched_getaffinity(tb_size_t pid, tb_cpuset_ref_t cpuset)
 {
     // check
     tb_assert_and_check_return_val(cpuset, tb_false);
 
-    // get cpu affinity
+#ifdef __NetBSD__
+    // NetBSD uses cpuset_t API for process affinity
+    cpuset_t *cpu_set = cpuset_create();
+    if (!cpu_set)
+        return tb_false;
+
+    if (cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, (pid_t)pid, cpuset_size(cpu_set), cpu_set) != 0)
+    {
+        cpuset_destroy(cpu_set);
+        return tb_false;
+    }
+
+    // save cpuset
+    tb_int_t i;
+    TB_CPUSET_ZERO(cpuset);
+    for (i = 0; i < TB_CPUSET_SIZE; i++)
+    {
+        if (cpuset_isset(i, cpu_set))
+            TB_CPUSET_SET(i, cpuset);
+    }
+    cpuset_destroy(cpu_set);
+    return tb_true;
+#else
+    // Linux uses cpu_set_t API
     cpu_set_t cpu_set;
     CPU_ZERO(&cpu_set);
     if (sched_getaffinity((pid_t)pid, sizeof(cpu_set_t), &cpu_set) != 0)
@@ -67,4 +110,5 @@ tb_bool_t tb_sched_getaffinity(tb_size_t
             TB_CPUSET_SET(i, cpuset);
     }
     return tb_true;
+#endif
 }
diff -pruN 1.7.8-1/src/tbox/platform/posix/semaphore.c 1.7.9-1/src/tbox/platform/posix/semaphore.c
--- 1.7.8-1/src/tbox/platform/posix/semaphore.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/platform/posix/semaphore.c	2025-11-25 14:31:14.000000000 +0000
@@ -26,6 +26,7 @@
 #include <time.h>
 #include <errno.h>
 #include <semaphore.h>
+#include "../time.h"
 
 /* //////////////////////////////////////////////////////////////////////////////////////
  * implementation
@@ -103,26 +104,76 @@ tb_long_t tb_semaphore_wait(tb_semaphore
     sem_t* h = (sem_t*)semaphore;
     tb_assert_and_check_return_val(h, -1);
 
-    // init time
-    struct timespec t = {0};
-    t.tv_sec = time(tb_null);
-    if (timeout > 0)
+    // non-blocking?
+    if (!timeout)
     {
-        t.tv_sec += timeout / 1000;
-        t.tv_nsec += (timeout % 1000) * 1000000;
+        while (sem_trywait(h))
+        {
+            if (errno == EINTR) continue;
+            return (errno == EAGAIN)? 0 : -1;
+        }
+        return 1;
     }
-    else if (timeout < 0) t.tv_sec += 12 * 30 * 24 * 3600; // infinity: one year
 
-    // wait semaphore
-    tb_long_t r = sem_timedwait(h, &t);
+    // infinite wait? use very large timeout and retry on timeout/interrupt
+    if (timeout < 0)
+    {
+        while (tb_true)
+        {
+            struct timespec ts;
+            if (clock_gettime(CLOCK_REALTIME, &ts))
+                return -1;
+
+            ts.tv_sec += (time_t)(30 * 24 * 3600); // one month ahead
+            // ensure nsec stays normalized
+            if (ts.tv_nsec >= 1000000000L)
+            {
+                ts.tv_sec += ts.tv_nsec / 1000000000L;
+                ts.tv_nsec %= 1000000000L;
+            }
+
+            if (!sem_timedwait(h, &ts))
+                return 1;
 
-    // ok?
-    tb_check_return_val(r, 1);
+            if (errno == EINTR)
+                continue;
+
+            if (errno == ETIMEDOUT || errno == EAGAIN)
+                continue; // we treat as infinite wait
+
+            return -1;
+        }
+    }
 
-    // timeout?
-    if (errno == EINTR || errno == EAGAIN || errno == ETIMEDOUT) return 0;
+    // finite timeout, loop until deadline or success
+    tb_hong_t deadline = tb_mclock() + timeout;
+    while (tb_true)
+    {
+        tb_hong_t remain = deadline - tb_mclock();
+        if (remain <= 0) return 0;
+
+        struct timespec ts;
+        if (clock_gettime(CLOCK_REALTIME, &ts))
+            return -1;
+
+        ts.tv_sec += (time_t)(remain / 1000);
+        ts.tv_nsec += (long)((remain % 1000) * 1000000);
+        if (ts.tv_nsec >= 1000000000L)
+        {
+            ts.tv_sec += 1;
+            ts.tv_nsec -= 1000000000L;
+        }
+
+        if (!sem_timedwait(h, &ts))
+            return 1;
+
+        if (errno == EINTR)
+            continue;
 
-    // error
-    return -1;
+        if (errno == ETIMEDOUT || errno == EAGAIN)
+            return 0;
+
+        return -1;
+    }
 }
 
diff -pruN 1.7.8-1/src/tbox/platform/posix/thread_affinity.c 1.7.9-1/src/tbox/platform/posix/thread_affinity.c
--- 1.7.8-1/src/tbox/platform/posix/thread_affinity.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/platform/posix/thread_affinity.c	2025-11-25 14:31:14.000000000 +0000
@@ -27,6 +27,9 @@
 #include "../thread.h"
 #include <pthread.h>
 #include <string.h>
+#ifdef __NetBSD__
+#include <sched.h>
+#endif
 
 /* //////////////////////////////////////////////////////////////////////////////////////
  * implementation
@@ -39,7 +42,23 @@ tb_bool_t tb_thread_setaffinity(tb_threa
     // get thread
     pthread_t pthread = thread? *((pthread_t*)thread) : pthread_self();
 
-    // set cpu affinity
+#ifdef __NetBSD__
+    // NetBSD uses cpuset_t API
+    cpuset_t *cpu_set = cpuset_create();
+    if (!cpu_set)
+        return tb_false;
+
+    tb_int_t i;
+    for (i = 0; i < TB_CPUSET_SIZE; i++)
+    {
+        if (TB_CPUSET_ISSET(i, cpuset))
+            cpuset_set(i, cpu_set);
+    }
+    tb_bool_t ok = pthread_setaffinity_np(pthread, cpuset_size(cpu_set), cpu_set) == 0;
+    cpuset_destroy(cpu_set);
+    return ok;
+#else
+    // Linux uses cpu_set_t API
     tb_int_t i;
     cpu_set_t cpu_set;
     CPU_ZERO(&cpu_set);
@@ -49,6 +68,7 @@ tb_bool_t tb_thread_setaffinity(tb_threa
             CPU_SET(i, &cpu_set);
     }
     return pthread_setaffinity_np(pthread, sizeof(cpu_set_t), &cpu_set) == 0;
+#endif
 }
 tb_bool_t tb_thread_getaffinity(tb_thread_ref_t thread, tb_cpuset_ref_t cpuset)
 {
@@ -58,7 +78,30 @@ tb_bool_t tb_thread_getaffinity(tb_threa
     // get thread
     pthread_t pthread = thread? *((pthread_t*)thread) : pthread_self();
 
-    // get cpu affinity
+#ifdef __NetBSD__
+    // NetBSD uses cpuset_t API
+    cpuset_t *cpu_set = cpuset_create();
+    if (!cpu_set)
+        return tb_false;
+
+    if (pthread_getaffinity_np(pthread, cpuset_size(cpu_set), cpu_set) != 0)
+    {
+        cpuset_destroy(cpu_set);
+        return tb_false;
+    }
+
+    // save cpuset
+    tb_int_t i;
+    TB_CPUSET_ZERO(cpuset);
+    for (i = 0; i < TB_CPUSET_SIZE; i++)
+    {
+        if (cpuset_isset(i, cpu_set))
+            TB_CPUSET_SET(i, cpuset);
+    }
+    cpuset_destroy(cpu_set);
+    return tb_true;
+#else
+    // Linux uses cpu_set_t API
     cpu_set_t cpu_set;
     CPU_ZERO(&cpu_set);
     if (pthread_getaffinity_np(pthread, sizeof(cpu_set_t), &cpu_set) != 0)
@@ -73,4 +116,5 @@ tb_bool_t tb_thread_getaffinity(tb_threa
             TB_CPUSET_SET(i, cpuset);
     }
     return tb_true;
+#endif
 }
diff -pruN 1.7.8-1/src/tbox/platform/systemv/semaphore.c 1.7.9-1/src/tbox/platform/systemv/semaphore.c
--- 1.7.8-1/src/tbox/platform/systemv/semaphore.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/platform/systemv/semaphore.c	2025-11-25 14:31:14.000000000 +0000
@@ -27,6 +27,7 @@
 #include <sys/types.h>
 #include <sys/sem.h>
 #include <sys/ipc.h>
+#include "../time.h"
 
 /* //////////////////////////////////////////////////////////////////////////////////////
  * implementation
@@ -116,29 +117,54 @@ tb_long_t tb_semaphore_wait(tb_semaphore
     tb_long_t h = (tb_long_t)semaphore - 1;
     tb_assert_and_check_return_val(semaphore, -1);
 
-    // init time
-    struct timeval t = {0};
-    if (timeout > 0)
-    {
-        t.tv_sec = timeout / 1000;
-        t.tv_usec = (timeout % 1000) * 1000;
-    }
-
-    // init
     struct sembuf sb;
     sb.sem_num = 0;
     sb.sem_op = -1;
     sb.sem_flg = SEM_UNDO;
 
-    // wait semaphore
-    tb_long_t r = semtimedop(h, &sb, 1, timeout >= 0? &t : tb_null);
+    // non-blocking?
+    if (!timeout)
+    {
+        struct timespec ts = {0, 0};
+        while (semtimedop(h, &sb, 1, &ts))
+        {
+            if (errno == EINTR) continue;
+            return (errno == EAGAIN)? 0 : -1;
+        }
+        return 1;
+    }
 
-    // ok?
-    tb_check_return_val(r, 1);
+    // infinite wait?
+    if (timeout < 0)
+    {
+        while (semop(h, &sb, 1))
+        {
+            if (errno == EINTR) continue;
+            return -1;
+        }
+        return 1;
+    }
 
-    // timeout or interrupted?
-    if (errno == EINTR || errno == EAGAIN) return 0;
+    // finite timeout
+    tb_hong_t deadline = tb_mclock() + timeout;
+    while (tb_true)
+    {
+        tb_hong_t remain = deadline - tb_mclock();
+        if (remain <= 0) return 0;
+
+        struct timespec ts;
+        ts.tv_sec = (time_t)(remain / 1000);
+        ts.tv_nsec = (long)((remain % 1000) * 1000000);
+
+        if (!semtimedop(h, &sb, 1, &ts))
+            return 1;
 
-    // error
-    return -1;
+        if (errno == EINTR)
+            continue;
+
+        if (errno == EAGAIN)
+            return 0;
+
+        return -1;
+    }
 }
diff -pruN 1.7.8-1/src/tbox/platform/windows/process.c 1.7.9-1/src/tbox/platform/windows/process.c
--- 1.7.8-1/src/tbox/platform/windows/process.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/platform/windows/process.c	2025-11-25 14:31:14.000000000 +0000
@@ -159,30 +159,85 @@ tb_void_t tb_process_group_exit()
     if (g_process_group)
         tb_kernel32()->TerminateJobObject(g_process_group, 0);
 }
+/*
+ * e.g.
+ * "C:\Program Files\app.exe"  -> "\"C:\\Program Files\\app.exe\""
+ * "path\to\file"              -> "path\to\file"
+ * "path with spaces"          -> "\"path with spaces\""
+ * "test\"quote"               -> "\"test\\\"quote\""
+ * "ends with backslash\"      -> "\"ends with backslash\\\\\""
+ *
+ * @see https://github.com/xmake-io/xmake/issues/6979
+ */
 static tb_void_t tb_process_args_append(tb_string_ref_t result, tb_char_t const* cstr)
 {
-    // need wrap quote?
+    // check if we need to wrap with quotes
+    // according to Windows command line argument rules
     tb_char_t ch;
     tb_char_t const* p = cstr;
     tb_bool_t wrap_quote = tb_false;
+    tb_bool_t empty = tb_true;
+
     while ((ch = *p))
     {
-        if (ch == ' ' || ch == '(' || ch == ')') wrap_quote = tb_true;
+        empty = tb_false;
+        // wrap if contains: space, tab, double quote, or empty string
+        if (ch == ' ' || ch == '\t' || ch == '\"')
+        {
+            wrap_quote = tb_true;
+            break;
+        }
         p++;
     }
 
+    // empty string also needs quotes
+    if (empty) wrap_quote = tb_true;
+
     // wrap begin quote
     if (wrap_quote) tb_string_chrcat(result, '\"');
 
-    // escape characters
+    // escape characters according to Windows rules:
+    // 1. Backslashes are interpreted literally, unless they immediately precede a double quote
+    // 2. A double quote preceded by a backslash is interpreted as a literal double quote
+    // 3. Backslashes are interpreted literally, unless they immediately precede a double quote
+    // 4. When followed by a double quote, backslashes must be doubled
     p = cstr;
     while ((ch = *p))
     {
-        // escape '"' or '\\'
-        if (ch == '\"' || (wrap_quote && ch == '\\'))
+        tb_size_t backslash_count = 0;
+
+        // count consecutive backslashes
+        while (ch == '\\')
+        {
+            backslash_count++;
+            p++;
+            ch = *p;
+        }
+
+        if (ch == '\"')
+        {
+            // backslashes before quote need to be doubled, plus escape the quote
+            for (tb_size_t i = 0; i < backslash_count * 2; i++)
+                tb_string_chrcat(result, '\\');
             tb_string_chrcat(result, '\\');
-        tb_string_chrcat(result, ch);
-        p++;
+            tb_string_chrcat(result, '\"');
+            p++;
+        }
+        else if (ch == '\0')
+        {
+            // backslashes at the end need to be doubled if we're wrapping with quotes
+            for (tb_size_t i = 0; i < (wrap_quote ? backslash_count * 2 : backslash_count); i++)
+                tb_string_chrcat(result, '\\');
+            break;
+        }
+        else
+        {
+            // normal backslashes don't need escaping
+            for (tb_size_t i = 0; i < backslash_count; i++)
+                tb_string_chrcat(result, '\\');
+            tb_string_chrcat(result, ch);
+            p++;
+        }
     }
 
     // wrap end quote
@@ -342,8 +397,15 @@ tb_process_ref_t tb_process_init_cmd(tb_
         }
 
         // redirect
-        HANDLE handlesToInherit[3];
+        // note: we may need up to 3 redirected handles + 3 standard handles = 6 total
+        HANDLE handlesToInherit[6];
         DWORD  handlesToInheritCount = 0;
+
+        // initialize all std handles to INVALID_HANDLE_VALUE
+        process->psi->hStdInput = INVALID_HANDLE_VALUE;
+        process->psi->hStdOutput = INVALID_HANDLE_VALUE;
+        process->psi->hStdError = INVALID_HANDLE_VALUE;
+
         if (attr)
         {
             // redirect from stdin
@@ -481,13 +543,155 @@ tb_process_ref_t tb_process_init_cmd(tb_
             }
         }
 
-        /* we just use the default std handles if lpAttributeList is not supported
+        /* if STARTF_USESTDHANDLES is set, we need to ensure all three handles are set
+         * for unset handles, use GetStdHandle() to get current standard handles
+         * but don't add them to handlesToInherit to avoid case1/case2 issues
          *
          * @see https://github.com/xmake-io/xmake/issues/3138#issuecomment-1338970250
          */
         if (bInheritHandle)
+        {
             process->psi->dwFlags |= STARTF_USESTDHANDLES;
 
+            // for unset handles, use GetStdHandle() to get current standard handles
+            // we need to duplicate and make them inheritable so child process can use them
+            // when using PROC_THREAD_ATTRIBUTE_HANDLE_LIST, we must also add these handles
+            // to the list, otherwise they won't be inherited even if set in StartupInfo
+            if (process->psi->hStdInput == INVALID_HANDLE_VALUE)
+            {
+                HANDLE hStdInput = GetStdHandle(STD_INPUT_HANDLE);
+                if (hStdInput != INVALID_HANDLE_VALUE && hStdInput != tb_null)
+                {
+                    HANDLE hDupInput = INVALID_HANDLE_VALUE;
+                    // duplicate handle and make it inheritable to avoid affecting parent process
+                    if (DuplicateHandle(GetCurrentProcess(), hStdInput, GetCurrentProcess(), &hDupInput, 0, TRUE, DUPLICATE_SAME_ACCESS))
+                    {
+                        process->psi->hStdInput = hDupInput;
+                        process->file_handles[process->file_handles_count++] = hDupInput;
+                        // add to handlesToInherit list so it can be inherited when using PROC_THREAD_ATTRIBUTE_HANDLE_LIST
+                        if (handlesToInheritCount < sizeof(handlesToInherit) / sizeof(handlesToInherit[0]))
+                            handlesToInherit[handlesToInheritCount++] = hDupInput;
+                    }
+                    else
+                    {
+                        // if duplication fails, try to make original inheritable (may affect parent)
+                        tb_kernel32()->SetHandleInformation(hStdInput, HANDLE_FLAG_INHERIT, TRUE);
+                        process->psi->hStdInput = hStdInput;
+                        if (handlesToInheritCount < sizeof(handlesToInherit) / sizeof(handlesToInherit[0]))
+                            handlesToInherit[handlesToInheritCount++] = hStdInput;
+                    }
+                }
+            }
+            if (process->psi->hStdOutput == INVALID_HANDLE_VALUE)
+            {
+                HANDLE hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
+                if (hStdOutput != INVALID_HANDLE_VALUE && hStdOutput != tb_null)
+                {
+                    HANDLE hDupOutput = INVALID_HANDLE_VALUE;
+                    // duplicate handle and make it inheritable to avoid affecting parent process
+                    if (DuplicateHandle(GetCurrentProcess(), hStdOutput, GetCurrentProcess(), &hDupOutput, 0, TRUE, DUPLICATE_SAME_ACCESS))
+                    {
+                        process->psi->hStdOutput = hDupOutput;
+                        process->file_handles[process->file_handles_count++] = hDupOutput;
+                        // add to handlesToInherit list so it can be inherited when using PROC_THREAD_ATTRIBUTE_HANDLE_LIST
+                        if (handlesToInheritCount < sizeof(handlesToInherit) / sizeof(handlesToInherit[0]))
+                            handlesToInherit[handlesToInheritCount++] = hDupOutput;
+                    }
+                    else
+                    {
+                        // if duplication fails, try to make original inheritable (may affect parent)
+                        tb_kernel32()->SetHandleInformation(hStdOutput, HANDLE_FLAG_INHERIT, TRUE);
+                        process->psi->hStdOutput = hStdOutput;
+                        if (handlesToInheritCount < sizeof(handlesToInherit) / sizeof(handlesToInherit[0]))
+                            handlesToInherit[handlesToInheritCount++] = hStdOutput;
+                    }
+                }
+            }
+            if (process->psi->hStdError == INVALID_HANDLE_VALUE)
+            {
+                HANDLE hStdError = GetStdHandle(STD_ERROR_HANDLE);
+                if (hStdError != INVALID_HANDLE_VALUE && hStdError != tb_null)
+                {
+                    HANDLE hDupError = INVALID_HANDLE_VALUE;
+                    // duplicate handle and make it inheritable to avoid affecting parent process
+                    if (DuplicateHandle(GetCurrentProcess(), hStdError, GetCurrentProcess(), &hDupError, 0, TRUE, DUPLICATE_SAME_ACCESS))
+                    {
+                        process->psi->hStdError = hDupError;
+                        process->file_handles[process->file_handles_count++] = hDupError;
+                        // add to handlesToInherit list so it can be inherited when using PROC_THREAD_ATTRIBUTE_HANDLE_LIST
+                        if (handlesToInheritCount < sizeof(handlesToInherit) / sizeof(handlesToInherit[0]))
+                            handlesToInherit[handlesToInheritCount++] = hDupError;
+                    }
+                    else
+                    {
+                        // if duplication fails, try to make original inheritable (may affect parent)
+                        tb_kernel32()->SetHandleInformation(hStdError, HANDLE_FLAG_INHERIT, TRUE);
+                        process->psi->hStdError = hStdError;
+                        if (handlesToInheritCount < sizeof(handlesToInherit) / sizeof(handlesToInherit[0]))
+                            handlesToInherit[handlesToInheritCount++] = hStdError;
+                    }
+                }
+            }
+
+            // update lpAttributeList if we added standard handles to handlesToInherit
+            // if lpAttributeList was already initialized (from earlier redirected handles), just update it
+            // otherwise, initialize it now
+            if (handlesToInheritCount > 0 && tb_kernel32()->InitializeProcThreadAttributeList)
+            {
+                if (lpAttributeListInited && lpAttributeList)
+                {
+                    // already initialized, try to update with new handle list
+                    // note: UpdateProcThreadAttribute may not support updating existing attribute list
+                    // so if update fails, we recreate the attribute list
+                    if (!tb_kernel32()->UpdateProcThreadAttribute(lpAttributeList, 0,
+                            PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
+                            handlesToInherit,
+                            handlesToInheritCount * sizeof(HANDLE), tb_null, tb_null))
+                    {
+                        // update failed, recreate the attribute list
+                        tb_kernel32()->DeleteProcThreadAttributeList(lpAttributeList);
+                        tb_free(lpAttributeList);
+                        lpAttributeList = tb_null;
+                        lpAttributeListInited = tb_false;
+                        // fall through to initialization code below
+                    }
+                }
+
+                if (!lpAttributeListInited)
+                {
+                    // not initialized yet (or recreate after failed update), initialize it now
+                    SIZE_T attributeListSize = 0;
+                    if (tb_kernel32()->InitializeProcThreadAttributeList(tb_null, 1, 0, &attributeListSize) ||
+                        GetLastError() == ERROR_INSUFFICIENT_BUFFER)
+                    {
+                        // if lpAttributeList was already allocated (from earlier), free it first
+                        if (lpAttributeList)
+                        {
+                            tb_free(lpAttributeList);
+                            lpAttributeList = tb_null;
+                        }
+
+                        if (!lpAttributeList)
+                            lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)tb_malloc(attributeListSize);
+
+                        if (lpAttributeList && tb_kernel32()->InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &attributeListSize))
+                        {
+                            lpAttributeListInited = tb_true;
+                            if (tb_kernel32()->UpdateProcThreadAttribute(lpAttributeList, 0,
+                                    PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
+                                    handlesToInherit,
+                                    handlesToInheritCount * sizeof(HANDLE), tb_null, tb_null))
+                            {
+                                process->si.lpAttributeList = lpAttributeList;
+                                flags |= EXTENDED_STARTUPINFO_PRESENT;
+                                bInheritHandle = tb_true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
         // init process security attributes
         SECURITY_ATTRIBUTES sap     = {0};
         sap.nLength                 = sizeof(SECURITY_ATTRIBUTES);
diff -pruN 1.7.8-1/src/tbox/stream/impl/stream/buffer.c 1.7.9-1/src/tbox/stream/impl/stream/buffer.c
--- 1.7.8-1/src/tbox/stream/impl/stream/buffer.c	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/tbox/stream/impl/stream/buffer.c	2025-11-25 14:31:14.000000000 +0000
@@ -99,7 +99,25 @@ static tb_long_t tb_stream_buffer_writ(t
     tb_check_return_val(data, -1);
     tb_check_return_val(size, 0);
 
-    if (size) tb_buffer_memncpyp(stream_buffer->buffer, stream_buffer->head, data, size);
+    /* ensure the buffer is large enough to hold the data at the write position
+     * tb_buffer_memncpyp will resize to head + size, which may truncate existing data
+     * so we need to ensure the buffer size is at least max(current_size, head + size)
+     *
+     * @see https://github.com/tboox/tbox/issues/208
+     */
+    if (size)
+    {
+        tb_size_t need_size = stream_buffer->head + size;
+        tb_size_t curr_size = tb_buffer_size(stream_buffer->buffer);
+        if (need_size > curr_size)
+        {
+            // expand the buffer
+            if (!tb_buffer_resize(stream_buffer->buffer, need_size)) return -1;
+        }
+        
+        // copy data at the write position
+        tb_memcpy(tb_buffer_data(stream_buffer->buffer) + stream_buffer->head, data, size);
+    }
     stream_buffer->head += size;
     return size;
 }
diff -pruN 1.7.8-1/src/xmake.sh 1.7.9-1/src/xmake.sh
--- 1.7.8-1/src/xmake.sh	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/src/xmake.sh	2025-11-25 14:31:14.000000000 +0000
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 set_project "tbox"
-set_version "1.7.7" "%Y%m%d" "1"
+set_version "1.7.9" "%Y%m%d" "1"
 
 # set warning all as error
 set_warnings "all" "error"
@@ -37,6 +37,8 @@ if is_plat "mingw" "msys" "cygwin"; then
     add_syslinks "ws2_32" "pthread" "m"
 elif is_plat "haiku"; then
     add_syslinks "pthread" "network" "m" "c"
+elif is_plat "bsd" "solaris"; then
+    add_syslinks "pthread" "m"
 else
     add_syslinks "pthread" "dl" "m" "c"
 fi
diff -pruN 1.7.8-1/xmake.lua 1.7.9-1/xmake.lua
--- 1.7.8-1/xmake.lua	2025-10-17 14:37:36.000000000 +0000
+++ 1.7.9-1/xmake.lua	2025-11-25 14:31:14.000000000 +0000
@@ -5,7 +5,7 @@ set_project("tbox")
 set_xmakever("2.8.2")
 
 -- set project version
-set_version("1.7.8", {build = "%Y%m%d", soname = true})
+set_version("1.7.9", {build = "%Y%m%d", soname = true})
 
 -- set warning all as error
 set_warnings("all", "error")
