tags: [niubash, platforms, unix, harmonyos] created: 2026-10-09 status: active

Platform Support

Which platforms Niubash runs on, what "runs" means for each, and the parts of the surface that are deliberately different per platform. This page is the contract; if a platform behaves differently from a claim here, that is a bug.

Status matrix

PlatformBuildSmoke-gatedReleasedNotes
Windows x64✅ windows-2025✅ release gate✅ .zip + .exeprimary target
Windows arm64✅ windows-2025✅ release gate✅
Linux x86_64 (glibc)✅ ubuntu-22.04✅ release gate✅ .tar.gzglibc ≥ 2.35
Linux aarch64 (glibc)✅ ubuntu-22.04-arm✅ release gate✅native arm64 runner
macOS aarch64✅ macos-latest✅ release gate✅
macOS x86_64✅ macos-latest✅ (Rosetta 2)✅Apple cross-compile
Linux x86_64 (musl)✅ ubuntu-latest✅ cross-target-smoke⬜static; see below
Android (aarch64-linux-android, armv7-linux-androideabi)✅ release build-android⬜⬜incubating: NDK-linked zip, no runtime smoke; see below
HarmonyOS PC (aarch64-linux-ohos)✅ ubuntu-latest check⬜⬜see below

"Smoke-gated" means the artifact is built, then run on its own OS, against the four checks in release.md before it is published — --version banner, -c execution, a pipeline through native coreutils, and the offline plugin stack. No smoke, no artifact.

musl

Compiled by cross-target-check and smoke-tested by cross-target-smoke, which builds the static musl binary and runs the same four checks natively (a static musl x86_64 binary runs on the glibc runner). The job asserts ldd reports statically linked first, so a silent re-link against the runner's glibc cannot pass as a musl build. No musl-tools package is needed: rustc links against the toolchain's own self-contained musl crt/libc through the host cc.

No musl release artifact ships yet. That is a delivery decision, not a coverage gap — flip it on by adding x86_64-unknown-linux-musl to the build-linux matrix in release.yml.

Android

The build-android legs in release.yml build aarch64-linux-android and armv7-linux-androideabi with a real bionic link through the NDK (nttld/setup-ndk, API 24 clang drivers). The bionic cfg surface was fixed upstream in rubash#456 (AT_EACCESS, confstr, sa_restorer, 32-bit off_t/S_IFMT widths). The legs are incubating: continue-on-error, not in the release needs, artifacts go to the workflow run only. The runner cannot execute bionic binaries, so the four-check smoke gate has not run for these — a zip from this leg is build-verified, not smoke-verified.

HarmonyOS

The Rust target is aarch64-unknown-linux-ohos. Two facts make it mostly free:

  • rustc --print cfg --target aarch64-unknown-linux-ohos reports target_os = "linux" and target_family = "unix". So Niubash's existing unix leg and rubash's existing Linux leg apply nearly unchanged, and fd/mod.rs's #[cfg(unix)] arm is taken as-is.
  • The target is Tier 2 with host tools in Rust, so a check-only build needs no external OpenHarmony SDK or clang wrapper.

The one compile blocker found so far was rubash's ulimit, which typed its resource selector as libc::__rlimit_resource_t — a typedef only glibc defines. musl and the OpenHarmony libc take a plain c_int. Fixed by aliasing on target_env (rubash#449). With that, the whole Niubash workspace compiles for aarch64-unknown-linux-ohos.

cross-target-check (in ci.yml) keeps it compiling. It is check-only because there is no HarmonyOS runtime available on GitHub runners, so the gate is "it compiles", not "it runs" — the ohos column of the status matrix stays ⬜ until a real device runs the smoke gate.

What HarmonyOS PC actually ships

An earlier revision of this page described HarmonyOS as "a de-GNU'd Linux distribution — musl libc, mksh as the default shell, toybox built in". That is wrong for HarmonyOS PC. It came from OpenHarmony's dev-board userland and was applied to the whole platform. The two are different products and the difference changes how much Niubash has to supply.

HarmonyOS PC is not stripped down. A measured install (HarmonyOS 1.12.0, aarch64, kernel HongMeng Kernel 1.12.0) runs bash as the shell (/data/service/hnp/bin/bash) and carries 454 commands under /data/service/hnp/bin — about 93% of a standard Linux command surface, and 100% of text processing:

grep sed awk cut sort uniq diff diff3 comm cmp head tail wc fold column

Also present: clang/gcc (multi-arch: aarch64 / armv7 / loongarch64 / x86_64), cmake, make, ninja, autoconf, bison, flex, gdb, lldb, git (+ git-lfs, git-svn), python3.12, node, full JDK, ruby, vim/nvim/nano. What is genuinely missing is system management, not text processing:

Missing / restrictedNote
apt / yum / dnfno classic package manager; use hnpcli or preinstalled
systemctlno systemd; HarmonyOS has its own service management
iptables / nftablesrestricted
dockernot preinstalled
snap / flatpakunsupported
ifconfig / netstatpresent but partly restricted (ip, ss preferred)

The HarmonyOS bash is stock GNU bash

It is worth pinning down what that bash is, because "it has bash" could mean a reimplementation. It is not. It is upstream GNU bash, cross-compiled against musl:

  • bash --version on a HarmonyOS PC prints GNU bash, version 5.1.16(1)-release — the GPL banner is GNU's own.
  • The public HNP build (Termony, build-hnp/bash/Makefile) pulls $(GNU_MIRROR)/gnu/bash/bash-5.3.tar.gz and configures it with --host aarch64-unknown-linux-musl --without-bash-malloc --disable-nls. --without-bash-malloc matters: it drops bash's bundled allocator so the musl allocator is used instead.
  • The build emits libsh.a, libhistory.a and the loadables (recho, printf, basename, …) — artifacts unique to the GNU bash source tree.

No Huawei-specific shell patches. It is an ordinary musl-linked GNU bash, which is a useful data point for us: the userland under HarmonyOS is a musl + Linux-ABI stack, not a bespoke one. That is why Niubash's target_os = "linux" cfgs all hit and why the port was two lines.

Note the version split when quoting numbers: the measured machine ran 5.1.16, while the public build recipe fetches 5.3. These are different devices at different times — do not present either as "the" HarmonyOS bash version.

Two userland caveats the existing ports hit

Both are worth knowing before a first device run, because neither is a documentation problem — they are things that actually broke people.

1. A third-party port still needed to bundle a shell toolset. Termony (a "Termux for HarmonyOS PC") ships 42 packages, and its list is aria2 bash binutils *busybox* ... gcc gdb gettext git ... — bash yes, coreutils no, busybox instead. So even with 454 commands present system- wide, an independent porter concluded the stock surface was not enough and vendored busybox to fill it. That is not proof the 93% number is wrong; it is evidence that "the tools exist" and "the tools are dependable" are different claims — the same distinction the argument below turns on.

2. Prefix mismatch inside the built-in Terminal. Termony's README warns:

you can use these utilities in the builtin Terminal app under /data/service/hnp: Although some paths might get wrong due to prefix set to /data/app/base.org/base_1.0 … You can override them like: LD_LIBRARY_PATH=/data/service/hnp/base.org/base_1.0/lib TERMINFO=/data/service/hnp/base.org/base_1.0/share/terminfo fish

This does not contradict the PATH finding below — PATH resolution and runtime prefix resolution are separate mechanisms. It does mean that a first device run should check more than "is it on PATH": also confirm that a resolved binary can actually find its own lib and data dirs. On HarmonyOS 6.0+ the README adds that sudo is available and system environment variables are editable, which is how the overrides are meant to be persisted.

The OpenHarmony dev boards are the limited case

There the userland is toybox, and Huawei's own documentation says it is trimmed per device:

In the current version, different devices support different toybox commands. You can run the toybox command to obtain the full list.

Community reports go further: toybox: Unknown command diff even though diff.c exists in third_party/toybox/toys/pending and is listed in BUILD.gn. The workaround people use is "选择使用 busybox 代替 toybox". The third-party pkgsrc bootstrap lists gawk and grep as required dependencies, which implies they are absent by default.

The honest split:

HarmonyOS PCOpenHarmony dev boards
Shellbashmksh
Command set454 commands at /data/service/hnp/bin (~93%)toybox, per-device trimmed
Text processingfully coveredgaps; awk/grep absent, diff may be "unknown" though the source exists
What Niubash addsa consistent bash + GNU-toolset baseline across OS versions, plus the Bash semanticsthe missing tools, not just the semantics

State the argument plainly, because it changed: on HarmonyOS PC the value is not "the commands are missing". It is that the toolset is not guaranteed uniform across device revisions, and that a bash-compatible runtime is still what you want when you do not control the host. Both are real, but weaker than "there is nothing here", and this page should not overstate them.

Packaging: HarmonyOS PC requires HNP

HarmonyOS PC does not accept the tarball layout the Linux legs ship. The constraint is documented for native ports and it is absolute:

在鸿蒙PC上,由于系统安全规格限制等原因,暂不支持通过"解压 + 配 PATH"的方式直接使用 tar.gz 包 ❌ 不能直接解压 tar.gz 包到任意目录 ❌ 不能通过设置 PATH 环境变量来使用 ✅ 必须打包成 HNP(HarmonyOS Native Package)格式才能正常使用

HNP = HarmonyOS Native Package, the platform's native package format — the analogue of MSI on Windows or deb/rpm on Linux. Packed with hnpcli (which ships in the OpenHarmony SDK):

hnpcli pack -i <pkg-dir> [-o <out-dir>]              # with hnp.json present
hnpcli pack -i <pkg-dir> -o <out-dir> -n <name> -v <ver>   # without

The payload is a directory with bin/, optional cfg/ and lib/, and a required hnp.json:

{
  "type": "hnp-config",
  "name": "hnpsample",
  "version": "1.1",
  "install": {
    "links": [
      { "source": "/bin/hnpsample", "target": "hnpsample" }
    ]
  }
}

source is relative to the package root and target becomes a symlink name in the bin directory. If links is omitted, every binary under bin/ gets a symlink by default. Constraints: no spaces or special characters in name/version, no non-ASCII directory names, package ≤ 4 GB. Binaries that need their own libraries should carry an rpath (-Wl,-rpath=${ORIGIN}/../lib).

The distribution path is the part that matters. An .hnp is not installed standalone. It is embedded in a HAP — an app bundle — declared in module.json5 as hnpPackages with "type": "public" or "private", and the HAP is signed and distributed through AppGallery:

HAP project root
└── hnp/                    # hnp root, as passed to --hnp-path
    └── arm64-v8a/          # device ABI dir
        ├── python.hnp
        └── sub_dir/test.hnp

So shipping Niubash to HarmonyOS PC is not "publish a .hnp and let people install it". It means publishing a HAP on the app store whose payload is Niubash's binaries. That is an app-store listing, a signature, and a review — the same shape of gate as the phone form factor, just with a looser runtime policy. Anyone who thought "HarmonyOS PC is a tarball" should recalibrate: the packaging is cheap, the distribution is a store release.

A public build framework exists — gitcode.com/OpenHarmonyPCDeveloper/build — which cross-compiles Linux/Unix CLI tools to aarch64-linux-ohos and packs them as HNP; it consumes OHOS_SDK, drives everything through build.sh --sdk <path>, and registers components in a dependency.json. Precedent components include tree and ninja.

The "same shape as the Linux leg" claim was wrong, twice over: the artifact is different (niu-<ver>-ohos.hnp, inside a HAP), and the delivery channel is different (store, not direct download).

Where HNP installs, and whether it lands on PATH

Two install roots, both of which do put their bin on PATH:

KindInstall pathBinaries symlinked intoOn PATH?
Public HNP/data/service/hnp/<name>_<version>/ (HNP_PUBLIC_HOME=/data/service/hnp)/data/service/hnp/bin✅ yes
Private HNP/data/app/<name>_<version>/ (HNP_PRIVATE_HOME=/data/app)/data/app/bin✅ yes

The official guide is explicit: "公有hnp包二进制软链接路径:/data/service/hnp/bin(已加入环境变量)", and the same for /data/app/bin. Where the two collide, HNP_PRIVATE_HOME wins — a same-named private binary shadows the public one.

So the open question from the previous revision is closed: a HNP does put its commands on PATH, and /data/service/hnp/bin — the same directory the 454-command measurement counted — is that symlink directory. Tools that install and then say "安装后找不到 ninja 命令?" and ask you to export PATH=... by hand have gone a manual route; that is not what the packaging system does by itself.

This is good news for Niubash: no Unix command-directory discovery mechanism is needed. Once the HAP is installed, the host looks like any other Unix box and Niubash's existing PATH-based discovery works unchanged. A real device should still run the smoke gate — but this particular unknown does not need one.

The build-framework side still uses HNP_PUBLIC_PATH when cross-compiling on a dev host (e.g. export HNP_PUBLIC_PATH=~/HarmonyOSPC/data/service/hnp) so hnpcli writes into the right tree; that is a build-host variable, not a runtime one.

uname on HarmonyOS

uname/arch do not hard-code a platform name. On unix, crates-side identity.rs reads uname(2) and prints utsname verbatim, the same way coreutils uname(1) does. So the reported values are whatever the running kernel says. Measured on real devices:

FieldFlagOpenHarmony (Linux kernel)HarmonyOS (Huawei kernel)
sysname-sLinuxHarmonyOS
nodename-nthe host namethe host name
release-rthe kernel releaseHongMeng Kernel 1.13.0
version-vthe build stamp#1 SMP Sat Aug 15 11:19:26 UTC 2026
machine-maarch64aarch64

-s being HarmonyOS (not Linux) is the one that bites. config.guess did not know it, fell through to "unable to guess system type", and exited non-zero — which breaks every autoconf-based configure, including LLVM's GetHostTriple.cmake. A patch proposing aarch64-unknown-linux-ohos for uname -s = HarmonyOS was filed upstream in 2026-08.

-r is a string with spaces. Code that splices uname -r into a compiler flag without quoting produces -D__HarmonyOS_HongMeng Kernel 1_12_0, which the shell splits into several arguments and Kernel is mistaken for an input file. That is a real bug in the wild (BitMagic 7.13.4), and the fix was to override the platform label, not to change the kernel.

There is nothing to special-case in Niubash, and that is deliberate: the honest source is the kernel, not a compile-time constant. Do not add a HarmonyOS-specific sysname() branch unless a concrete consumer needs it — and if something does need to branch, it should match HarmonyOS exactly, not Linux.

$BASH_VERSION reports Niubash's Bash-compatibility level (e.g. 5.3.0(1)-release) on every platform, HarmonyOS included.

Sandbox quirks worth knowing on HarmonyOS

Beyond the kernel name, HarmonyOS restricts the filesystem in ways a portable shell has to survive. Measured on HarmonyOS 7 (aarch64): /tmp is read-only and $HOME is not writable, so a probe that opens a temp file gets Permission denied. That is why a naive config.guess fails twice on the same box — once on the temp file, and again on the unrecognised OS name. Applications also face binary code-signing verification, which is why third-party executables on HarmonyOS PC frequently hit Permission denied on launch.

A shell that assumes a writable $TMPDIR or a writable $HOME will misbehave here. Niubash should be checked against both on the first real device run; nothing in the build changes.

None of this changes the build. It is listed here so nobody spends a day re-deriving it.

HarmonyOS PC vs HarmonyOS phone

These are two different products, not one port.

HarmonyOS PCHarmonyOS phone
Userlandfull (bash + 454 commands)full
fork/exec of new processesunrestrictedrestricted — ordinary apps cannot create processes directly; the app must declare each one in its config and pass per-process review at listing time
PackagingHNP (tarball + PATH is not supported)HNP, same format
DistributionHAP on AppGallery — an HNP is embedded in a HAP, not installed standaloneHAP on AppGallery
Statusthe target this page describesa new form factor; not in scope yet

The fork restriction is the load-bearing runtime difference. Huawei's own FAQ (faqs-ability-112) states that on phones the system restricts and manages fork for ordinary apps, and community reports add that PC does not restrict it. Niubash is built on a fork+exec model on unix (see rubash's fd/unix.rs: Command::new + Stdio::from_raw_fd + pre_exec), so PC is a plain port while the phone form factor needs the process-declaration path to be sustainable before it is worth building.

But note what the distribution column now says: both are HAP. The packaging and store-release work is largely the same for the two form factors; what differs is the runtime process policy. That is a different split from the earlier "PC is a tarball / phone is an app" framing, and it is the accurate one.

Do not treat "HarmonyOS support" as one item. The store-release work is shared, but the phone form factor adds a per-process declaration and review gate that PC does not — and that gate is not something Niubash controls.

What niu is not: no MSYS runtime

On Windows niu prints MSYS labels: uname -s reports MSYS_NT-…, $OSTYPE is msys, $MACHTYPE is x86_64-pc-msys. Those labels are a compatibility persona (rubash#154), not a runtime. Ecosystem scripts routinely gate on uname -s matching MINGW*|MSYS*|CYGWIN* or on OSTYPE = msys before they run; the persona exists so those gates open. It is a default, not a fact — RUBASH_IDENTITY=native switches the builtins to the native face (uname -s → Windows_NT, $MACHTYPE → x86_64-pc-windows).

Under the labels there is no MSYS2 runtime: no msys-2.0.dll, no cygwin1.dll, and nothing inside a child process that converts POSIX argv for Windows. niu is a pure native Windows build; every process it starts is an ordinary Win32 process, and the POSIX-form argv an MSYS runtime would rewrite in-child is instead rewritten by niu's own executor before that process starts — the distinction the behavioral boundary below states. So the uname story on this page splits by platform: on unix the builtin reports the kernel verbatim (see uname on HarmonyOS); on Windows it reports persona labels that name a runtime which is not there.

The behavioral boundary (measured)

A script written for real MSYS assumesniu does
The runtime rewrites POSIX-form argv before a native child sees it.It does — /c/Users/cmx reaches a native child as C:\Users\cmx (python -c 'import sys; print(sys.argv[1])' /c/Users/cmx). The exception is a native command processor: cmd /c echo /d/repo prints /d/repo, because cmd owns /C-style switches.
/d/... reaches the child as D:\....For a native child, yes — git -C /d/repo/niubash rev-parse --show-toplevel prints D:/repo/niubash, and python -c 'import sys; print(sys.argv[1])' /d/repo prints D:\repo. A native command processor is again the exception (see above).
An MSYS2 root: /etc/fstab, /etc/profile, MSYS2's /usr./ is winuxcmd's installation root (ls / lists its usr, bin, etc, opt, …), not the current drive's root; the Unix-shaped /etc resolves inside that tree to a real Windows directory holding only winuxcmd's mtab shim.
MSYS2's bundled perl/awk runtimes.No perl at all; awk is winuxcmd's own native build, not MSYS2's.

The root row is the one that still bites: niu's / is the WinuxCmd installation root, so a script that walks out from / sees the winuxcmd tree (usr, bin, etc, opt, …) rather than a drive root, and there is no MSYS2 /etc/profile or /etc/fstab. On the argv rows niu now matches what an MSYS2-targeted script assumes: POSIX-form argv is rewritten for native children, and shell-namespace roots such as /usr/bin, /etc, and /tmp are mapped for every child class. The one carve-out is the native command processor, which keeps its slash-prefixed drive-form operands verbatim. Unlike an MSYS runtime, though, this is not a layer inside the child: it is niu's own path handling, applied by the Rubash executor at the child boundary and documented in the Windows Path Contract (the internal __RUBASH_ARGV_DIALECT=legacy switch restores the earlier existence-gated behaviour).

One line of orientation: Git Bash = MSYS2 runtime + GNU bash; niu = native engine + compatibility persona. The labels match so ecosystem scripts run; the runtime they name is not present.

Calling PowerShell from niu

niu is a full bash, so every command string it runs (through niu -c '...', a script file, or piped stdin) goes through expansion first, and bash expands $ in double-quoted and unquoted text. PowerShell uses $ for the same slot: $var, $args, $env:TEMP. Call PowerShell without quoting discipline and niu expands PowerShell's variables before PowerShell starts. An undefined niu variable expands to the empty string, so this:

niu -c 'powershell -NoProfile -Command "$x=1; Write-Output x=$x"'

hands PowerShell =1; Write-Output x=, which fails with =1 : The term '=1' is not recognized. $x was never a niu variable, so bash replaced both occurrences with nothing.

This is standard bash semantics, not a quoting bug; the ruling and the reproduction cases are in niubash#203. The fix belongs to the calling side:

1. Escape the $. Inside double quotes, \$ survives expansion as a literal dollar sign:

niu -c 'powershell -NoProfile -Command "\$x=1; Write-Output x=\$x"'
# prints x=1

It works, but every PowerShell $ now owes a backslash, and the one you miss turns into an empty string; the error you get names the mangled command, not the missing backslash.

2. Keep the $ inside bash single quotes. Bash never expands inside single quotes. From an interactive niu prompt, one pair does the whole job:

powershell -NoProfile -Command '$x=1; Write-Output x=$x'

Through niu -c the quoting doubles, because the -c payload is itself a shell string to whatever shell launches niu. Once you are counting backslashes at two levels, use the file route instead.

3. Write a .ps1 file and run it with -File (recommended). The script's text never passes through bash, so nothing needs escaping. The $t, $o, $env:TEMP case from #203's real-world report runs as written:

niu -c 'powershell -NoProfile -ExecutionPolicy Bypass -File C:/scripts/scan.ps1'

-ExecutionPolicy Bypass is not optional polish. Windows clients ship with the script execution policy set to Restricted, which refuses .ps1 scripts with "running scripts is disabled on this system"; the flag lifts that check for the one process and changes nothing outside it.

The rule itself is bash's, not PowerShell's: any target that puts $ in double-quoted or unquoted command text is subject to it.

What is Windows-only

By compile-time cfg, the following never appear on non-Windows paths and must not be weakened (no wpm strings off Windows):

  • WinuxCmd command links and the wpm package-manager surface.
  • The ConPTY interactive journey harness.
  • Windows path translation (/mnt/X drive argv rewriting) and the Windows signal bridge.

Unix tarballs carry only niu + README.md + LICENSE; native system tools are used as-is rather than bundled.