feat: add HarmonyOS OpenHarmony support

Harvest the HarmonyOS/OpenHarmony port from PR #2634 and make it publish-safe by target-gating unsupported host dependencies out of the OHOS TUI graph. Self-update is disabled on OHOS, PTY shell mode reports unsupported, and Starlark execpolicy parsing returns an explicit unsupported-platform error until upstream starlark/rustyline/nix support catches up.

Add OHOS SDK setup docs and launcher scripts, install the rustls ring provider for rustls-no-provider entrypoints, and keep the packaged codewhale-tui OHOS graph free of starlark, rustyline, nix@0.28, portable-pty, and arboard.

Validation: cargo fmt --all -- --check; git diff --check; git diff --cached --check; cargo check -p codewhale-cli --locked; cargo check -p codewhale-app-server --locked; cargo check -p codewhale-tui --locked; cargo test -p codewhale-cli --locked update::tests::; cargo test -p codewhale-release --locked; cargo test -p codewhale-tui --locked background_tty_command_has_controlling_terminal; cargo test -p codewhale-tui --locked clipboard; cargo package -p codewhale-tui --allow-dirty --no-verify --locked; packaged OHOS cargo tree checks. OHOS target check still requires a loaded OpenHarmony SDK/sysroot and currently stops in ring with missing assert.h when CC/CFLAGS/linker are unset.

Harvested from PR #2634 by @shenjackyuanjie.

Co-authored-by: shenjackyuanjie <54507071+shenjackyuanjie@users.noreply.github.com>
This commit is contained in:
Hunter B
2026-06-03 20:52:35 -07:00
parent 5f51f89c76
commit 23c9481af1
39 changed files with 784 additions and 431 deletions
+18
View File
@@ -0,0 +1,18 @@
# HarmonyOS/OpenHarmony cross-build paths are intentionally not configured
# here. Cargo does not expand environment variables inside target linker paths
# or CMake toolchain paths, so checked-in absolute SDK paths make the workspace
# machine-specific.
#
# See docs/HarmonyOS.md for setup details.
#
# Set OHOS_NATIVE_SDK to the OpenHarmony native SDK directory, then load one of:
#
# PowerShell:
# . .\scripts\ohos-env.ps1
#
# Linux/macOS:
# . ./scripts/ohos-env.sh
#
# The setup scripts export Cargo's target-specific linker, AR, CC, CXX, CFLAGS,
# CXXFLAGS, CARGO_ENCODED_RUSTFLAGS, CC_SHELL_ESCAPED_FLAGS, and
# CMAKE_TOOLCHAIN_FILE variables for aarch64-unknown-linux-ohos.
+2
View File
@@ -50,6 +50,8 @@ docs/*.pdf
# Local dev scripts and temp files
*.sh
*.cmd
!ohos-clang.sh
!ohos-clangxx.sh
!scripts/**
!.github/scripts/**
test.txt
+10 -1
View File
@@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`/restore` now shows the 20 most recent snapshots, numeric restore targets can
reach beyond that default listing up to a bounded index, and list requests
above the visible cap fail explicitly instead of silently truncating.
- Added HarmonyOS/OpenHarmony support scaffolding: environment-driven
`OHOS_NATIVE_SDK` setup scripts and compiler wrappers, platform docs,
explicit Rustls ring-provider installation for the no-provider TLS build, and
OHOS fallbacks for unsupported keyring, clipboard, sandbox, browser-open, TTY,
execpolicy Starlark parsing, and self-update surfaces.
### Changed
@@ -33,13 +38,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
allowlist approval is merged.
- Documented the agent and sub-agent stewardship ethos so future automation
preserves human issue intake, careful PR review, and contributor credit.
- Moved the TUI Starlark execpolicy parser and PTY support behind non-OHOS
target dependencies so published OpenHarmony builds no longer pull `nix` 0.28
through `rustyline` or `portable-pty`.
### Community
Thanks to **@cyq1017** for the restore-listing implementation (#2513) and
**@wywsoor** for the broader macOS/iTerm rollback UX report (#2494), and
**@HUQIANTAO** for the `web_run` lock-splitting work (#2502) and turn-metadata
prefix-cache stability work (#2517).
prefix-cache stability work (#2517), and **@shenjackyuanjie** for the
HarmonyOS/OpenHarmony port and MatePad Edge validation trail (#2634).
## [0.8.53] - 2026-06-03
Generated
+114 -300
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -38,7 +38,8 @@ chrono = { version = "0.4.43", features = ["serde"] }
clap = { version = "4.5.54", features = ["derive"] }
clap_complete = "4.5"
dirs = "6.0.0"
reqwest = { version = "0.13.1", default-features = false, features = ["json", "rustls", "socks"] }
reqwest = { version = "0.13.1", default-features = false, features = ["json", "rustls-no-provider", "socks"] }
rustls = { version = "0.23.36", default-features = false, features = ["ring", "std", "tls12"] }
rusqlite = { version = "0.32.1", features = ["bundled"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
+2
View File
@@ -143,6 +143,8 @@ codewhale doctor # セットアップを検証
`npm i -g codewhale` は v0.8.8 以降、glibc ベースの ARM64 Linux で動作します。[Releases ページ](https://github.com/Hmbown/CodeWhale/releases) からビルド済みバイナリをダウンロードし、`PATH` 上に並べて配置することもできます。
HarmonyOS PC と OpenHarmony クロスビルドの設定は [docs/HarmonyOS.md](docs/HarmonyOS.md) を参照してください。
### 中国 / ミラーフレンドリーなインストール
中国本土から GitHub または npm のダウンロードが遅い場合は、Cargo レジストリのミラーを利用してください:
+2
View File
@@ -221,6 +221,8 @@ Prebuilt binary pairs and platform archives are published for Linux x64, Linux
ARM64, macOS x64, macOS ARM64, and Windows x64. For other targets, see
[docs/INSTALL.md](docs/INSTALL.md).
For HarmonyOS PC and OpenHarmony cross-build setup, see [docs/HarmonyOS.md](docs/HarmonyOS.md).
### China / Mirror-friendly Installation
If GitHub or npm downloads are slow from mainland China, use
+2
View File
@@ -183,6 +183,8 @@ Hãy chỉ định mô hình hoặc cấp độ suy nghĩ cố định nếu b
Lệnh cài đặt `npm i -g codewhale` hoạt động trên môi trường Linux ARM64 nền glibc từ phiên bản v0.8.8 trở đi. Bạn cũng có thể tải trực tiếp các tệp binary dựng sẵn từ [trang phát hành Releases](https://github.com/Hmbown/CodeWhale/releases) và đặt chúng cạnh nhau trong một thư mục thuộc biến `PATH`.
Xem [docs/HarmonyOS.md](docs/HarmonyOS.md) để cấu hình HarmonyOS PC và cross-build OpenHarmony.
### Cài đặt thân thiện qua Mirror (Tại Trung Quốc)
Nếu việc tải xuống từ GitHub hoặc npm bị chậm từ Trung Quốc đại lục, bạn hãy sử dụng mirror registry cho Cargo:
+2
View File
@@ -186,6 +186,8 @@ Auto 模式同时控制两个设置:
从 v0.8.8 起,`npm i -g codewhale` 直接支持 glibc 系的 ARM64 Linux。你也可以从 [Releases 页面](https://github.com/Hmbown/CodeWhale/releases) 下载预编译二进制,放到 `PATH` 目录中。
HarmonyOS PC 运行和 OpenHarmony 交叉编译配置见 [docs/HarmonyOS.md](docs/HarmonyOS.md)。
### 中国大陆 / 镜像友好安装
如果在中国大陆访问 GitHub 或 npm 下载较慢,可以通过 Cargo 注册表镜像安装:
+1
View File
@@ -21,6 +21,7 @@ codewhale-state = { path = "../state", version = "0.8.53" }
codewhale-tools = { path = "../tools", version = "0.8.53" }
serde.workspace = true
serde_json.workspace = true
rustls.workspace = true
tokio.workspace = true
tower-http.workspace = true
uuid.workspace = true
+6
View File
@@ -27,6 +27,8 @@ struct Cli {
#[tokio::main]
async fn main() -> Result<()> {
install_rustls_crypto_provider();
let cli = Cli::parse();
let listen: SocketAddr = format!("{}:{}", cli.host, cli.port)
.parse()
@@ -41,6 +43,10 @@ async fn main() -> Result<()> {
.await
}
fn install_rustls_crypto_provider() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
fn app_server_token_from_env() -> Option<String> {
std::env::var("CODEWHALE_APP_SERVER_TOKEN")
.ok()
+1
View File
@@ -38,6 +38,7 @@ dirs.workspace = true
serde.workspace = true
serde_json.workspace = true
reqwest = { workspace = true, features = ["blocking"] }
rustls.workspace = true
semver.workspace = true
tokio.workspace = true
sha2.workspace = true
+6
View File
@@ -471,7 +471,13 @@ struct AppServerArgs {
const MCP_SERVER_DEFINITIONS_KEY: &str = "mcp.server_definitions";
fn install_rustls_crypto_provider() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
pub fn run_cli() -> std::process::ExitCode {
install_rustls_crypto_provider();
match run() {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(err) => {
+8
View File
@@ -20,6 +20,12 @@ use std::io::Write;
/// Run the self-update workflow.
pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Result<()> {
#[cfg(target_env = "ohos")]
{
let _ = (beta, check_only, proxy_arg);
bail!("self-update is not supported on HarmonyOS/OpenHarmony yet");
}
let current_exe =
std::env::current_exe().context("failed to determine current executable path")?;
let targets = update_targets_for_exe(&current_exe);
@@ -353,6 +359,8 @@ pub(crate) fn validate_and_build_proxy(proxy_str: &str) -> Result<Proxy> {
}
fn update_http_client(proxy: Option<&Proxy>) -> Result<reqwest::blocking::Client> {
let _ = rustls::crypto::ring::default_provider().install_default();
let mut builder = reqwest::blocking::Client::builder();
if let Some(proxy) = proxy {
builder = builder.proxy(proxy.clone());
+1
View File
@@ -9,6 +9,7 @@ description = "Shared CodeWhale release discovery and version comparison helpers
[dependencies]
anyhow.workspace = true
reqwest = { workspace = true, features = ["blocking"] }
rustls.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
+1 -1
View File
@@ -19,7 +19,7 @@ keyring = { version = "3", features = ["apple-native"] }
[target.'cfg(target_os = "windows")'.dependencies]
keyring = { version = "3", features = ["windows-native"] }
[target.'cfg(target_os = "linux")'.dependencies]
[target.'cfg(all(target_os = "linux", not(target_env = "ohos")))'.dependencies]
keyring = { version = "3", features = ["linux-native-sync-persistent", "crypto-rust"] }
[dev-dependencies]
+46 -10
View File
@@ -92,7 +92,7 @@ pub trait KeyringStore: Send + Sync {
/// Wraps the platform credential store:
/// - **macOS**: Keychain (via `security` framework)
/// - **Windows**: Credential Manager
/// - **Linux**: Secret Service (GNOME Keyring / kwallet via dbus)
/// - **Linux**: Secret Service (GNOME Keyring / kwallet via dbus), excluding OHOS
///
/// This backend is opt-in -- set the [`SECRET_BACKEND_ENV`] environment
/// variable to `system` or `keyring` to activate it. On platforms without
@@ -124,7 +124,11 @@ impl DefaultKeyringStore {
/// Probe the OS keyring without writing anything. Returns `Ok(())` if
/// a backend is reachable, otherwise an error describing why not.
pub fn probe(&self) -> Result<(), SecretsError> {
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
#[cfg(any(
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
{
// `Entry::new` is enough to validate the native macOS/Windows
// backend path. Avoid a dummy read there because it can trigger
@@ -149,7 +153,11 @@ impl DefaultKeyringStore {
Err(other) => Err(SecretsError::Keyring(other.to_string())),
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
#[cfg(not(any(
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
)))]
{
let _ = &self.service;
Err(SecretsError::Keyring(unsupported_keyring_message()))
@@ -159,7 +167,11 @@ impl DefaultKeyringStore {
impl KeyringStore for DefaultKeyringStore {
fn get(&self, key: &str) -> Result<Option<String>, SecretsError> {
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
#[cfg(any(
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
{
let entry = keyring::Entry::new(&self.service, key)
.map_err(|err| SecretsError::Keyring(err.to_string()))?;
@@ -169,7 +181,11 @@ impl KeyringStore for DefaultKeyringStore {
Err(err) => Err(SecretsError::Keyring(err.to_string())),
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
#[cfg(not(any(
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
)))]
{
let _ = key;
Err(SecretsError::Keyring(unsupported_keyring_message()))
@@ -177,7 +193,11 @@ impl KeyringStore for DefaultKeyringStore {
}
fn set(&self, key: &str, value: &str) -> Result<(), SecretsError> {
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
#[cfg(any(
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
{
let entry = keyring::Entry::new(&self.service, key)
.map_err(|err| SecretsError::Keyring(err.to_string()))?;
@@ -185,7 +205,11 @@ impl KeyringStore for DefaultKeyringStore {
.set_password(value)
.map_err(|err| SecretsError::Keyring(err.to_string()))
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
#[cfg(not(any(
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
)))]
{
let _ = (key, value);
Err(SecretsError::Keyring(unsupported_keyring_message()))
@@ -193,7 +217,11 @@ impl KeyringStore for DefaultKeyringStore {
}
fn delete(&self, key: &str) -> Result<(), SecretsError> {
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
#[cfg(any(
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
{
let entry = keyring::Entry::new(&self.service, key)
.map_err(|err| SecretsError::Keyring(err.to_string()))?;
@@ -202,7 +230,11 @@ impl KeyringStore for DefaultKeyringStore {
Err(err) => Err(SecretsError::Keyring(err.to_string())),
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
#[cfg(not(any(
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
)))]
{
let _ = key;
Err(SecretsError::Keyring(unsupported_keyring_message()))
@@ -214,7 +246,11 @@ impl KeyringStore for DefaultKeyringStore {
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
#[cfg(not(any(
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
)))]
fn unsupported_keyring_message() -> String {
"system keyring backend is unsupported on this platform".to_string()
}
+9 -5
View File
@@ -26,7 +26,6 @@ path = "src/bin/deepseek_tui_legacy_shim.rs"
[dependencies]
anyhow = "1.0.100"
arboard = "3.4"
codewhale-config = { path = "../config", version = "0.8.53" }
codewhale-protocol = { path = "../protocol", version = "0.8.53" }
codewhale-release = { path = "../release", version = "0.8.53" }
@@ -47,10 +46,10 @@ fd-lock = "4.0.4"
futures-util = "0.3.31"
ratatui = "0.30"
regex = "1.11"
reqwest = { version = "0.13.1", default-features = false, features = ["blocking", "json", "stream", "multipart", "form", "rustls", "http2", "gzip", "brotli"] }
reqwest = { version = "0.13.1", default-features = false, features = ["blocking", "json", "stream", "multipart", "form", "rustls-no-provider", "http2", "gzip", "brotli"] }
rustls.workspace = true
qrcode = { version = "0.14", default-features = false }
similar = "2"
rustyline = "15.0.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.149", features = ["preserve_order"] }
schemars = { version = "1.2.1", features = ["derive", "preserve_order"] }
@@ -71,9 +70,7 @@ tower-http = { version = "0.6", features = ["cors"] }
wait-timeout = "0.2"
multimap = "0.10.0"
shlex = "1.3.0"
starlark = "0.13.0"
tiny_http = "0.12"
portable-pty = "0.9"
zeroize = "1.8.2"
ignore = "0.4"
image = { version = "0.25", default-features = false, features = ["png"] }
@@ -91,6 +88,13 @@ vt100 = "0.15"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[target.'cfg(any(target_os = "macos", target_os = "windows", all(target_os = "linux", not(target_env = "ohos"))))'.dependencies]
arboard = "3.4"
[target.'cfg(not(target_env = "ohos"))'.dependencies]
portable-pty = "0.9"
starlark = "0.13.0"
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.3"
objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSArray", "NSDictionary", "NSError", "NSObject", "NSString", "NSURL"] }
+1
View File
@@ -62,6 +62,7 @@ where
}
}
#[cfg(not(target_env = "ohos"))]
pub fn apply_to_pty_command<I, K, V>(cmd: &mut portable_pty::CommandBuilder, overrides: I)
where
I: IntoIterator<Item = (K, V)>,
+4
View File
@@ -1,3 +1,4 @@
#[cfg(not(target_env = "ohos"))]
use starlark::Error as StarlarkError;
use thiserror::Error;
@@ -23,6 +24,9 @@ pub enum Error {
},
#[error("expected example to not match rule `{rule}`: {example}")]
ExampleDidMatch { rule: String, example: String },
#[error("{0}")]
UnsupportedPlatform(String),
#[error("starlark error: {0}")]
#[cfg(not(target_env = "ohos"))]
Starlark(StarlarkError),
}
+6
View File
@@ -6,7 +6,10 @@ pub mod decision;
pub mod error;
pub mod execpolicycheck;
pub mod matcher;
#[cfg(not(target_env = "ohos"))]
pub mod parser;
#[cfg(target_env = "ohos")]
pub mod parser_ohos;
pub mod policy;
pub mod rule;
pub mod rules;
@@ -17,7 +20,10 @@ pub use decision::Decision;
pub use error::Error;
pub use error::Result;
pub use execpolicycheck::ExecPolicyCheckCommand;
#[cfg(not(target_env = "ohos"))]
pub use parser::PolicyParser;
#[cfg(target_env = "ohos")]
pub use parser_ohos::PolicyParser;
pub use policy::Evaluation;
pub use policy::Policy;
pub use rule::Rule;
+26
View File
@@ -0,0 +1,26 @@
use super::error::Error;
use super::error::Result;
pub struct PolicyParser;
impl Default for PolicyParser {
fn default() -> Self {
Self::new()
}
}
impl PolicyParser {
pub fn new() -> Self {
Self
}
pub fn parse(&mut self, _policy_identifier: &str, _policy_file_contents: &str) -> Result<()> {
Err(Error::UnsupportedPlatform(
"Starlark execpolicy files are not supported on HarmonyOS/OpenHarmony yet because upstream starlark-rust still depends on a rustyline/nix chain that does not compile for OHOS.".to_string(),
))
}
pub fn build(self) -> super::policy::Policy {
super::policy::Policy::empty()
}
}
+5
View File
@@ -109,6 +109,10 @@ fn configure_windows_console_utf8() {
#[cfg(not(windows))]
fn configure_windows_console_utf8() {}
fn install_rustls_crypto_provider() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
#[derive(Parser, Debug)]
#[command(
name = "codewhale-tui",
@@ -846,6 +850,7 @@ enum SandboxCommand {
#[tokio::main]
async fn main() -> Result<()> {
configure_windows_console_utf8();
install_rustls_crypto_provider();
// ── Process hardening (#2183) ─────────────────────────────────────────
// MUST run before Tokio is booted and before any threads are spawned.
+23 -17
View File
@@ -35,13 +35,13 @@ pub mod process_hardening;
#[cfg(target_os = "macos")]
pub mod seatbelt;
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
pub mod landlock;
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
pub mod seccomp;
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
pub mod bwrap;
#[cfg(target_os = "windows")]
@@ -223,7 +223,7 @@ pub enum SandboxType {
MacosSeatbelt,
/// Linux Landlock sandboxing (kernel 5.13+).
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
LinuxLandlock,
/// Windows process-containment helper.
@@ -240,7 +240,7 @@ impl std::fmt::Display for SandboxType {
SandboxType::None => write!(f, "none"),
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => write!(f, "macos-seatbelt"),
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
SandboxType::LinuxLandlock => write!(f, "linux-landlock"),
#[cfg(target_os = "windows")]
SandboxType::Windows => write!(f, "windows-sandbox"),
@@ -305,7 +305,7 @@ pub fn get_platform_sandbox() -> Option<SandboxType> {
}
}
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
{
if landlock::is_available() {
return Some(SandboxType::LinuxLandlock);
@@ -410,7 +410,7 @@ impl SandboxManager {
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => Self::prepare_seatbelt(spec),
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
SandboxType::LinuxLandlock => self.prepare_landlock(spec),
#[cfg(target_os = "windows")]
@@ -467,7 +467,7 @@ impl SandboxManager {
/// If `prefer_bwrap` is set and `/usr/bin/bwrap` is available, routes the
/// command through bubblewrap for stronger filesystem isolation (#2184).
/// Otherwise falls back to Landlock markers.
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
fn prepare_landlock(&self, spec: &CommandSpec) -> ExecEnv {
// Check if bwrap passthrough should be used (#2184).
if self.prefer_bwrap && bwrap::is_available() {
@@ -539,7 +539,10 @@ impl SandboxManager {
/// This helps distinguish between legitimate command failures and
/// sandbox-blocked operations.
pub fn was_denied(sandbox_type: SandboxType, exit_code: i32, stderr: &str) -> bool {
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
#[cfg(not(any(
target_os = "macos",
all(target_os = "linux", not(target_env = "ohos"))
)))]
let _ = (exit_code, stderr);
match sandbox_type {
@@ -548,7 +551,7 @@ impl SandboxManager {
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => seatbelt::detect_denial(exit_code, stderr),
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
SandboxType::LinuxLandlock => landlock::detect_denial(exit_code, stderr),
#[cfg(target_os = "windows")]
@@ -558,7 +561,10 @@ impl SandboxManager {
/// Get a human-readable description of why a command was blocked.
pub fn denial_message(sandbox_type: SandboxType, stderr: &str) -> String {
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
#[cfg(not(any(
target_os = "macos",
all(target_os = "linux", not(target_env = "ohos"))
)))]
let _ = stderr;
match sandbox_type {
@@ -578,7 +584,7 @@ impl SandboxManager {
}
}
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
SandboxType::LinuxLandlock => {
// Seccomp patterns checked first because they are more specific (#2182).
if stderr.contains("Bad system call")
@@ -825,7 +831,7 @@ mod tests {
}
#[test]
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
fn test_parity_linux_landlock_available() {
let st = get_platform_sandbox();
assert!(matches!(st, Some(SandboxType::LinuxLandlock)));
@@ -844,7 +850,7 @@ mod tests {
0,
""
));
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
assert!(!SandboxManager::was_denied(
SandboxType::LinuxLandlock,
0,
@@ -855,7 +861,7 @@ mod tests {
}
#[test]
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
fn test_parity_seccomp_sigsys_detected() {
assert!(SandboxManager::was_denied(
SandboxType::LinuxLandlock,
@@ -891,7 +897,7 @@ mod tests {
let spec = CommandSpec::shell("true", PathBuf::from("/tmp"), Duration::from_secs(5))
.with_policy(SandboxPolicy::default());
let env = manager.prepare(&spec);
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
{
let marker = env.env.get("DEEPSEEK_SANDBOX");
assert!(marker.is_none_or(|v| v != "bwrap"));
@@ -905,7 +911,7 @@ mod tests {
let spec = CommandSpec::shell("true", PathBuf::from("/tmp"), Duration::from_secs(5))
.with_policy(SandboxPolicy::default());
let env = manager.prepare(&spec);
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
{
if crate::sandbox::bwrap::is_available() {
let marker = env.env.get("DEEPSEEK_SANDBOX");
+3 -3
View File
@@ -45,18 +45,18 @@
/// hardening is defense-in-depth — the sandbox still protects child processes
/// even if these prctls fail (e.g., in a container where some are restricted).
pub fn apply_process_hardening() {
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
{
apply_linux_hardening();
}
#[cfg(not(target_os = "linux"))]
#[cfg(not(all(target_os = "linux", not(target_env = "ohos"))))]
{
tracing::debug!("Process hardening skipped: not on Linux");
}
}
/// Linux-specific hardening implementation.
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
fn apply_linux_hardening() {
// ── PR_SET_DUMPABLE = 0 ────────────────────────────────────────────────
//
+4 -4
View File
@@ -155,18 +155,18 @@ fn probe_git(workspace: &Path) -> GitProbe {
}
fn probe_bwrap_available() -> bool {
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
{
crate::sandbox::bwrap::is_available()
}
#[cfg(not(target_os = "linux"))]
#[cfg(not(all(target_os = "linux", not(target_env = "ohos"))))]
{
false
}
}
fn probe_cgroup_version() -> Option<u8> {
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
{
let path = std::path::Path::new("/sys/fs/cgroup/cgroup.controllers");
if path.exists() {
@@ -178,7 +178,7 @@ fn probe_cgroup_version() -> Option<u8> {
}
None
}
#[cfg(not(target_os = "linux"))]
#[cfg(not(all(target_os = "linux", not(target_env = "ohos"))))]
{
None
}
+94 -58
View File
@@ -34,6 +34,7 @@ use windows::Win32::System::JobObjects::{
#[cfg(windows)]
use windows::core::PCWSTR;
#[cfg(not(target_env = "ohos"))]
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use super::shell_output::{summarize_output, truncate_with_meta};
@@ -130,6 +131,7 @@ pub struct ShellDeltaResult {
enum ShellChild {
Process(Child),
#[cfg(not(target_env = "ohos"))]
Pty(Box<dyn portable_pty::Child + Send>),
}
@@ -165,7 +167,7 @@ fn kill_child_process_group(child: &mut Child) -> std::io::Result<()> {
/// path (`kill_child_process_group` from the cancellation token) still
/// handles normal shutdown; abnormal exit can leak children — tracked as a
/// follow-up watchdog item per the original issue's acceptance criteria.
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
fn install_parent_death_signal(cmd: &mut Command) {
use std::os::unix::process::CommandExt;
// SAFETY: `pre_exec` runs in the child between fork and exec. The closure
@@ -227,7 +229,7 @@ fn push_shell_args(cmd: &mut Command, _program: &str, args: &[String]) {
cmd.args(args);
}
#[cfg(not(target_os = "linux"))]
#[cfg(not(all(target_os = "linux", not(target_env = "ohos"))))]
fn install_parent_death_signal(_cmd: &mut Command) {
// No kernel-level equivalent on macOS / Windows. The cooperative
// cancellation + process_group SIGKILL path covers normal shutdown;
@@ -363,6 +365,7 @@ impl ShellExitStatus {
}
}
#[cfg(not(target_env = "ohos"))]
fn from_pty(status: portable_pty::ExitStatus) -> Self {
let code = i32::try_from(status.exit_code()).unwrap_or(i32::MAX);
Self {
@@ -378,6 +381,7 @@ impl ShellChild {
ShellChild::Process(child) => child
.try_wait()
.map(|status| status.map(ShellExitStatus::from_std)),
#[cfg(not(target_env = "ohos"))]
ShellChild::Pty(child) => child
.try_wait()
.map(|status| status.map(ShellExitStatus::from_pty)),
@@ -387,6 +391,7 @@ impl ShellChild {
fn wait(&mut self) -> std::io::Result<ShellExitStatus> {
match self {
ShellChild::Process(child) => child.wait().map(ShellExitStatus::from_std),
#[cfg(not(target_env = "ohos"))]
ShellChild::Pty(child) => child.wait().map(ShellExitStatus::from_pty),
}
}
@@ -397,6 +402,7 @@ impl ShellChild {
ShellChild::Process(child) => kill_child_process_group(child),
#[cfg(not(unix))]
ShellChild::Process(child) => child.kill(),
#[cfg(not(target_env = "ohos"))]
ShellChild::Pty(child) => child.kill(),
}
}
@@ -404,6 +410,7 @@ impl ShellChild {
enum StdinWriter {
Pipe(ChildStdin),
#[cfg(not(target_env = "ohos"))]
Pty(Box<dyn Write + Send>),
}
@@ -411,6 +418,7 @@ impl StdinWriter {
fn write_all(&mut self, data: &[u8]) -> std::io::Result<()> {
match self {
StdinWriter::Pipe(stdin) => stdin.write_all(data),
#[cfg(not(target_env = "ohos"))]
StdinWriter::Pty(writer) => writer.write_all(data),
}
}
@@ -418,6 +426,7 @@ impl StdinWriter {
fn flush(&mut self) -> std::io::Result<()> {
match self {
StdinWriter::Pipe(stdin) => stdin.flush(),
#[cfg(not(target_env = "ohos"))]
StdinWriter::Pty(writer) => writer.flush(),
}
}
@@ -523,8 +532,14 @@ impl BackgroundShell {
// Without this kill, handle.join() blocks indefinitely, freezing the UI
// event loop that calls list_jobs() → poll() → collect_output().
#[cfg(unix)]
if let Some(ShellChild::Process(ref mut proc)) = self.child {
let _ = kill_child_process_group(proc);
if let Some(child) = self.child.as_mut() {
match child {
ShellChild::Process(proc) => {
let _ = kill_child_process_group(proc);
}
#[cfg(not(target_env = "ohos"))]
ShellChild::Pty(_) => {}
}
}
#[cfg(windows)]
terminate_and_close_windows_job(self.windows_job.take());
@@ -619,21 +634,25 @@ impl BackgroundShell {
/// Kill the process
fn kill(&mut self) -> Result<()> {
if let Some(ref mut child) = self.child {
if let ShellChild::Process(proc) = child {
#[cfg(windows)]
{
terminate_windows_job(self.windows_job.as_ref(), proc)
.context("Failed to kill process tree")?;
let _ = proc.wait();
match child {
ShellChild::Process(proc) => {
#[cfg(windows)]
{
terminate_windows_job(self.windows_job.as_ref(), proc)
.context("Failed to kill process tree")?;
let _ = proc.wait();
}
#[cfg(not(windows))]
{
proc.kill().context("Failed to kill process")?;
let _ = proc.wait();
}
}
#[cfg(not(windows))]
{
proc.kill().context("Failed to kill process")?;
let _ = proc.wait();
#[cfg(not(target_env = "ohos"))]
ShellChild::Pty(child) => {
child.kill().context("Failed to kill process")?;
let _ = child.wait();
}
} else {
child.kill().context("Failed to kill process")?;
let _ = child.wait();
}
}
self.status = ShellStatus::Killed;
@@ -717,10 +736,14 @@ impl Drop for BackgroundShell {
&& let Some(ref mut child) = self.child
{
#[cfg(windows)]
if let ShellChild::Process(proc) = child {
let _ = terminate_windows_job(self.windows_job.as_ref(), proc);
} else {
let _ = child.kill();
match child {
ShellChild::Process(proc) => {
let _ = terminate_windows_job(self.windows_job.as_ref(), proc);
}
#[cfg(not(target_env = "ohos"))]
ShellChild::Pty(child) => {
let _ = child.kill();
}
}
#[cfg(not(windows))]
let _ = child.kill();
@@ -1276,6 +1299,13 @@ impl ShellManager {
let program = exec_env.program();
let args = exec_env.args();
#[cfg(target_env = "ohos")]
if tty {
return Err(anyhow!(
"TTY shell mode is not supported on HarmonyOS/OpenHarmony yet."
));
}
let stdout_buffer = Arc::new(Mutex::new(Vec::new()));
let stderr_buffer = if tty {
None
@@ -1287,45 +1317,51 @@ impl ShellManager {
let mut windows_job = None;
let (child, stdin, stdout_thread, stderr_thread) = if tty {
let pty_system = native_pty_system();
let pair = pty_system
.openpty(PtySize {
rows: 24,
cols: 80,
pixel_width: 0,
pixel_height: 0,
})
.context("Failed to open PTY")?;
#[cfg(target_env = "ohos")]
unreachable!("OHOS TTY mode returns before PTY setup");
let mut cmd = CommandBuilder::new(program);
for arg in args {
cmd.arg(arg);
#[cfg(not(target_env = "ohos"))]
{
let pty_system = native_pty_system();
let pair = pty_system
.openpty(PtySize {
rows: 24,
cols: 80,
pixel_width: 0,
pixel_height: 0,
})
.context("Failed to open PTY")?;
let mut cmd = CommandBuilder::new(program);
for arg in args {
cmd.arg(arg);
}
cmd.cwd(working_dir);
child_env::apply_to_pty_command(&mut cmd, child_env::string_map_env(&exec_env.env));
let child = pair
.slave
.spawn_command(cmd)
.with_context(|| format!("Failed to spawn PTY command: {original_command}"))?;
drop(pair.slave);
let reader = pair
.master
.try_clone_reader()
.context("Failed to clone PTY reader")?;
let stdout_thread = Some(spawn_reader_thread(reader, Arc::clone(&stdout_buffer)));
let writer = pair
.master
.take_writer()
.context("Failed to take PTY writer")?;
(
ShellChild::Pty(child),
Some(StdinWriter::Pty(writer)),
stdout_thread,
None,
)
}
cmd.cwd(working_dir);
child_env::apply_to_pty_command(&mut cmd, child_env::string_map_env(&exec_env.env));
let child = pair
.slave
.spawn_command(cmd)
.with_context(|| format!("Failed to spawn PTY command: {original_command}"))?;
drop(pair.slave);
let reader = pair
.master
.try_clone_reader()
.context("Failed to clone PTY reader")?;
let stdout_thread = Some(spawn_reader_thread(reader, Arc::clone(&stdout_buffer)));
let writer = pair
.master
.take_writer()
.context("Failed to take PTY writer")?;
(
ShellChild::Pty(child),
Some(StdinWriter::Pty(writer)),
stdout_thread,
None,
)
} else {
let mut cmd = Command::new(program);
push_shell_args(&mut cmd, program, args);
+1 -1
View File
@@ -285,7 +285,7 @@ fn test_write_stdin_streams_output() {
}
#[test]
#[cfg(unix)]
#[cfg(all(unix, not(target_env = "ohos")))]
fn background_tty_command_has_controlling_terminal() {
let tmp = tempdir().expect("tempdir");
let mut manager = ShellManager::new(tmp.path().to_path_buf());
+99 -23
View File
@@ -12,17 +12,34 @@ use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
#[cfg(any(
all(test, unix),
all(
any(target_os = "macos", target_os = "windows", target_os = "linux"),
not(test)
)
all(not(test), target_os = "macos"),
all(not(test), target_os = "windows"),
all(not(test), target_os = "linux", not(target_env = "ohos"))
))]
use std::process::{Command, Stdio};
#[cfg(any(
test,
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result, bail};
#[cfg(any(
test,
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
use arboard::{Clipboard, ImageData};
use base64::Engine as _;
#[cfg(any(
test,
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
use image::{ImageBuffer, Rgba};
const OSC52_MAX_BYTES: usize = 100 * 1024;
@@ -53,6 +70,7 @@ impl PastedImage {
}
/// Clipboard payloads supported by the TUI.
#[cfg_attr(all(target_env = "ohos", not(test)), allow(dead_code))]
pub enum ClipboardContent {
Text(String),
Image(PastedImage),
@@ -60,7 +78,19 @@ pub enum ClipboardContent {
/// Clipboard reader/writer helper.
pub struct ClipboardHandler {
#[cfg(any(
test,
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
clipboard: Option<Clipboard>,
#[cfg(any(
test,
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
clipboard_init_attempted: bool,
#[cfg(test)]
written_text: Vec<String>,
@@ -74,7 +104,19 @@ impl ClipboardHandler {
/// server (headless, WSL2) never blocks the TUI event loop.
pub fn new() -> Self {
Self {
#[cfg(any(
test,
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
clipboard: None,
#[cfg(any(
test,
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
clipboard_init_attempted: false,
#[cfg(test)]
written_text: Vec::new(),
@@ -89,6 +131,12 @@ impl ClipboardHandler {
/// temporary thread and give it 500 ms; if it doesn't return in time the
/// handler stays in fallback/no-op mode and `read`/`write_text` fall
/// through to their OSC 52 and pbcopy/powershell fallbacks.
#[cfg(any(
test,
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
fn ensure_clipboard(&mut self) {
if self.clipboard_init_attempted {
return;
@@ -110,23 +158,32 @@ impl ClipboardHandler {
/// `workspace` is used as a fallback location when `~/.codewhale/` cannot
/// be resolved (e.g. running with a stripped HOME in CI sandboxes).
pub fn read(&mut self, workspace: &Path) -> Option<ClipboardContent> {
#[cfg(all(target_os = "linux", not(test)))]
#[cfg(all(target_os = "linux", not(target_env = "ohos"), not(test)))]
if let Ok(text) = read_text_with_wlpaste() {
return Some(ClipboardContent::Text(text));
}
self.ensure_clipboard();
let clipboard = self.clipboard.as_mut()?;
if let Ok(text) = clipboard.get_text() {
return Some(ClipboardContent::Text(text));
}
if let Ok(image) = clipboard.get_image()
&& let Ok(pasted) = save_image_as_png(workspace, &image)
#[cfg(any(
test,
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
{
return Some(ClipboardContent::Image(pasted));
self.ensure_clipboard();
let clipboard = self.clipboard.as_mut()?;
if let Ok(text) = clipboard.get_text() {
return Some(ClipboardContent::Text(text));
}
if let Ok(image) = clipboard.get_image()
&& let Ok(pasted) = save_image_as_png(workspace, &image)
{
return Some(ClipboardContent::Image(pasted));
}
}
let _ = workspace;
None
}
@@ -140,16 +197,23 @@ impl ClipboardHandler {
#[cfg(not(test))]
{
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
if write_text_with_wlcopy(text).is_ok() {
return Ok(());
}
self.ensure_clipboard();
if let Some(clipboard) = self.clipboard.as_mut()
&& clipboard.set_text(text.to_string()).is_ok()
#[cfg(any(
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
{
return Ok(());
self.ensure_clipboard();
if let Some(clipboard) = self.clipboard.as_mut()
&& clipboard.set_text(text.to_string()).is_ok()
{
return Ok(());
}
}
#[cfg(target_os = "macos")]
@@ -215,17 +279,17 @@ fn write_text_with_stdin_command(
Ok(())
}
#[cfg(all(target_os = "linux", not(test)))]
#[cfg(all(target_os = "linux", not(target_env = "ohos"), not(test)))]
fn write_text_with_wlcopy(text: &str) -> Result<()> {
write_text_with_wlcopy_using_argv("wl-copy", text)
}
#[cfg(all(target_os = "linux", not(test)))]
#[cfg(all(target_os = "linux", not(target_env = "ohos"), not(test)))]
fn read_text_with_wlpaste() -> Result<String> {
read_text_with_wlpaste_using_argv("wl-paste")
}
#[cfg(any(all(test, unix), target_os = "linux"))]
#[cfg(any(all(test, unix), all(target_os = "linux", not(target_env = "ohos"))))]
fn read_text_with_wlpaste_using_argv(program: &str) -> Result<String> {
let output = Command::new(program)
.arg("--no-newline")
@@ -241,7 +305,7 @@ fn read_text_with_wlpaste_using_argv(program: &str) -> Result<String> {
String::from_utf8(output.stdout).context("wl-paste returned non-UTF-8 text")
}
#[cfg(all(target_os = "linux", not(test)))]
#[cfg(all(target_os = "linux", not(target_env = "ohos"), not(test)))]
fn write_text_with_wlcopy_using_argv(program: &str, text: &str) -> Result<()> {
let mut child = Command::new(program)
.stdin(Stdio::piped())
@@ -310,12 +374,24 @@ fn clipboard_images_dir_for_home(workspace: &Path, home: Option<&Path>) -> PathB
/// Encode an RGBA `ImageData` from arboard as PNG and persist it. Returns
/// the resulting path along with metadata used to render the paste hint.
#[cfg(any(
test,
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
fn save_image_as_png(workspace: &Path, image: &ImageData) -> Result<PastedImage> {
save_image_as_png_in(&clipboard_images_dir(workspace), image)
}
/// Lower-level variant that writes into an explicit directory. Exposed so the
/// unit tests don't have to scribble inside the user's real home directory.
#[cfg(any(
test,
target_os = "macos",
target_os = "windows",
all(target_os = "linux", not(target_env = "ohos"))
))]
fn save_image_as_png_in(dir: &Path, image: &ImageData) -> Result<PastedImage> {
std::fs::create_dir_all(dir).context("create clipboard-images dir")?;
+7 -3
View File
@@ -242,7 +242,7 @@ fn browser_open_command(url: &str) -> Result<Command> {
Ok(command)
}
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
{
let mut command = Command::new("xdg-open");
command.arg(url);
@@ -256,7 +256,11 @@ fn browser_open_command(url: &str) -> Result<Command> {
Ok(cmd)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
#[cfg(not(any(
target_os = "macos",
all(target_os = "linux", not(target_env = "ohos")),
target_os = "windows"
)))]
Err(anyhow::anyhow!(
"browser opening is unsupported on this platform"
))
@@ -863,7 +867,7 @@ mod project_mapping_tests {
);
}
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
{
assert_eq!(command.get_program(), "xdg-open");
assert_eq!(
+78
View File
@@ -0,0 +1,78 @@
# HarmonyOS and OpenHarmony
This page covers CodeWhale on HarmonyOS PC and OpenHarmony cross-build setups.
## Running On HarmonyOS PC
HarmonyOS PC can use the normal Linux ARM64 package when its userspace is
glibc-compatible:
```bash
npm i -g codewhale
codewhale --version
```
You can also download `codewhale-linux-arm64` and
`codewhale-tui-linux-arm64` from the GitHub Releases page and place both
binaries on `PATH`.
## Cross-Compiling To OpenHarmony
The repository does not check in machine-specific SDK paths. Set
`OHOS_NATIVE_SDK` to the OpenHarmony native SDK directory, the directory that
contains `llvm/bin`, `sysroot`, and `build/cmake/ohos.toolchain.cmake`.
On Windows PowerShell:
```powershell
$env:OHOS_NATIVE_SDK="<path-to-openharmony-native-sdk>"
. .\scripts\ohos-env.ps1
rustup target add aarch64-unknown-linux-ohos
cargo build --target aarch64-unknown-linux-ohos -p codewhale-cli
```
On Linux or macOS:
```bash
export OHOS_NATIVE_SDK=/path/to/openharmony/native
. ./scripts/ohos-env.sh
rustup target add aarch64-unknown-linux-ohos
cargo build --target aarch64-unknown-linux-ohos -p codewhale-cli
```
The setup scripts export Cargo's target-specific `linker`, `AR`, `CC`, `CXX`,
`CFLAGS`, `CXXFLAGS`, `CARGO_ENCODED_RUSTFLAGS`, `CC_SHELL_ESCAPED_FLAGS`, and
CMake toolchain variables for `aarch64-unknown-linux-ohos`.
## Compiler Wrappers
For ad-hoc compiler calls, use the root wrappers. They read the same
`OHOS_NATIVE_SDK` variable and do not contain local paths.
Windows PowerShell:
```powershell
.\ohos-clang.ps1 --version
.\ohos-clangxx.ps1 --version
```
Linux or macOS:
```bash
sh ./ohos-clang.sh --version
sh ./ohos-clangxx.sh --version
```
If you want to run the POSIX wrappers directly as `./ohos-clang.sh`, make them
executable first:
```bash
chmod +x ./ohos-clang.sh ./ohos-clangxx.sh
```
## Cargo Config
`.cargo/config.toml` intentionally does not set a checked-in linker path.
Cargo cannot expand environment variables inside `linker` or CMake toolchain
path values there, so those values are exported by `scripts/ohos-env.ps1` and
`scripts/ohos-env.sh` instead.
+2
View File
@@ -44,6 +44,8 @@ systems such as Alpine should use [Build from source](#7-build-from-source).
> and `codewhale-tui-linux-arm64`, so a plain `npm i -g codewhale` works
> on any glibc-based ARM64 Linux. If you're stuck on v0.8.7, jump to
> [Build from source](#7-build-from-source) — `cargo install` works fine.
> For HarmonyOS PC and OpenHarmony cross-build setup, see
> [HarmonyOS and OpenHarmony](HarmonyOS.md).
---
+5 -4
View File
@@ -19,8 +19,9 @@ PR is harvested, superseded, deferred, or closed.
1. Stabilization and PR harvest: finish #2721 and #2722 before new feature work.
2. Provider/model/auth correctness: land narrow correctness fixes that match the
current provider architecture.
3. HarmonyOS/MatePad Edge intake: keep #2634 active, scoped, and credited while
the OHOS/Nix dependency clearance work finishes upstream.
3. HarmonyOS/MatePad Edge intake: keep #2634 credited while the local harvest
clears the OHOS/Nix dependency chain; full target-build success still needs a
host with the OpenHarmony native SDK loaded.
4. File decomposition Phase 1: split safe, test-covered config/provider and TUI
view surfaces before adding larger workflow UX.
5. WhaleFlow MVP: typed IR, executor skeleton, replay, and pod monitor before
@@ -42,7 +43,7 @@ harvest/stewardship commits:
| #2708 Windows sub-agent completion halves TUI render width | Cherry-picked as `e933a11d7`; follow-up fix `72653f8ef` invalidates reused fanout-card rows. | `cargo test -p codewhale-tui --locked subagent`; `cargo test -p codewhale-tui --locked terminal_size`; `cargo clippy -p codewhale-tui --locked -- -D warnings` passed. |
| #2627 Xiaomi MiMo Token Plan mode | Harvested only the auth-header behavior as `5aa68d986`; did not merge the conflicting mode/env changes. | `cargo test -p codewhale-tui --bin codewhale-tui --locked xiaomi_mimo`; `cargo test -p codewhale-secrets --locked xiaomi_mimo`; `cargo test -p codewhale-config --locked xiaomi_mimo`; `cargo clippy -p codewhale-tui --locked -- -D warnings` passed. |
| #2636 project-context mtime cache | Defer direct merge; harvest only after cache key/signature is widened. | Must include constitution changes, auto-generated context deletion, canonical path equivalence, and overwrite detection before landing. |
| #2634 HarmonyOS port | Active HarmonyOS/MatePad Edge lane; do not close. | User-supplied MatePad Edge demo (`https://bilibili.com/video/av116689597368905`) confirms real-device interest. PR remains draft/blocked while the author waits on upstream Nix/dependency clearance and carries local patches; full port needs OHOS target checks plus sandbox, TLS, keyring, clipboard, browser-open, and self-update review before merge. |
| #2634 HarmonyOS port | Locally harvested with additional Nix-chain clearance; keep credited and do not close until the integration branch is public. | User-supplied MatePad Edge demo (`https://bilibili.com/video/av116689597368905`) confirms real-device interest. Added env-driven OpenHarmony SDK setup, OHOS platform guards/fallbacks, self-update disablement, and OHOS target gating for Starlark execpolicy parsing plus PTY support so published OHOS builds do not pull `nix` 0.28 through `rustyline` or `portable-pty`. `cargo check --workspace --all-features --locked`, focused PTY/clipboard tests, and `cargo tree --locked -p codewhale-tui --target aarch64-unknown-linux-ohos -i nix@0.28.0` passed; full OHOS target check is blocked on this host because `OHOS_NATIVE_SDK`/target CC/sysroot are not configured and `ring` cannot find `assert.h`. |
| #2687 append-only mode/approval prompt | Defer direct merge; draft has compile failures and Plan-mode prompt correctness risks. | Any future harvest must keep stable `message[0]` genuinely mode-agnostic, preserve mode/approval suffixes after capacity replans, and distinguish external overrides from persisted generated prompts. |
| #2581 provider fallback chain design doc | Manually harvested as `docs/rfcs/2574-provider-fallback-chain.md` because the current PR head has no net file changes. | Keep issue #2574 open for implementation; close/comment on #2581 after the integration branch is public, crediting @idling11 and reporter @hsdbeebou. |
| #2530 mention depth-cap hint | Already present in the current v0.9 stack as `a97675824` and `29f57665e`. | `cargo test -p codewhale-tui --locked try_autocomplete_file_mention_no_match` passed. |
@@ -99,7 +100,7 @@ harvest/stewardship commits:
| #2631 estimated_input_tokens cache | Mergeable | Already harvested into the 22-commit stack. |
| #2632 tool-catalog JSON cache | Mergeable | Already harvested into the 22-commit stack. |
| #2633 capacity reverse scans | Mergeable | Already harvested into the 22-commit stack. |
| #2634 HarmonyOS port | Draft/blocked | Keep as active HarmonyOS/MatePad Edge lane. Do not merge wholesale until upstream Nix/dependency clearance, OHOS target checks, and sandbox/TLS/keyring/clipboard/browser/self-update review are complete. |
| #2634 HarmonyOS port | Draft / locally harvested | Harvested with credit and extra Nix-chain fixes. Keep the original PR open for now; comment after the integration branch is public and request a real OHOS SDK build confirmation from the contributor before closing. |
| #2635 output rows cache | Mergeable | Already harvested into the 22-commit stack. |
| #2636 project-context cache | Conflicting | Defer/harvest only after cache correctness fixes. |
| #2639 POST /v1/sessions endpoint | Mergeable | Defer; app-server contract needs focused review. |
+23
View File
@@ -0,0 +1,23 @@
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($env:OHOS_NATIVE_SDK)) {
[Console]::Error.WriteLine("error: set OHOS_NATIVE_SDK to the OpenHarmony native SDK directory. It must contain llvm\bin and sysroot.")
exit 1
}
$sdk = $env:OHOS_NATIVE_SDK
$clang = [System.IO.Path]::Combine($sdk, "llvm", "bin", "clang.exe")
$sysroot = [System.IO.Path]::Combine($sdk, "sysroot")
if (-not (Test-Path -LiteralPath $clang -PathType Leaf -ErrorAction SilentlyContinue)) {
[Console]::Error.WriteLine("error: OHOS_NATIVE_SDK does not contain llvm\bin\clang.exe: $sdk")
exit 1
}
if (-not (Test-Path -LiteralPath $sysroot -PathType Container -ErrorAction SilentlyContinue)) {
[Console]::Error.WriteLine("error: OHOS_NATIVE_SDK does not contain sysroot: $sdk")
exit 1
}
& $clang -target aarch64-linux-ohos "--sysroot=$sysroot" -D__MUSL__ @args
exit $LASTEXITCODE
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env sh
set -eu
if [ -z "${OHOS_NATIVE_SDK:-}" ]; then
echo "error: set OHOS_NATIVE_SDK to the OpenHarmony native SDK directory. It must contain llvm/bin and sysroot." >&2
exit 1
fi
sdk=$OHOS_NATIVE_SDK
clang=$sdk/llvm/bin/clang
sysroot=$sdk/sysroot
if [ ! -x "$clang" ]; then
echo "error: OHOS_NATIVE_SDK does not contain executable llvm/bin/clang: $sdk" >&2
exit 1
fi
if [ ! -d "$sysroot" ]; then
echo "error: OHOS_NATIVE_SDK does not contain sysroot: $sdk" >&2
exit 1
fi
exec "$clang" -target aarch64-linux-ohos "--sysroot=$sysroot" -D__MUSL__ "$@"
+23
View File
@@ -0,0 +1,23 @@
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($env:OHOS_NATIVE_SDK)) {
[Console]::Error.WriteLine("error: set OHOS_NATIVE_SDK to the OpenHarmony native SDK directory. It must contain llvm\bin and sysroot.")
exit 1
}
$sdk = $env:OHOS_NATIVE_SDK
$clangxx = [System.IO.Path]::Combine($sdk, "llvm", "bin", "clang++.exe")
$sysroot = [System.IO.Path]::Combine($sdk, "sysroot")
if (-not (Test-Path -LiteralPath $clangxx -PathType Leaf -ErrorAction SilentlyContinue)) {
[Console]::Error.WriteLine("error: OHOS_NATIVE_SDK does not contain llvm\bin\clang++.exe: $sdk")
exit 1
}
if (-not (Test-Path -LiteralPath $sysroot -PathType Container -ErrorAction SilentlyContinue)) {
[Console]::Error.WriteLine("error: OHOS_NATIVE_SDK does not contain sysroot: $sdk")
exit 1
}
& $clangxx -target aarch64-linux-ohos "--sysroot=$sysroot" -D__MUSL__ @args
exit $LASTEXITCODE
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env sh
set -eu
if [ -z "${OHOS_NATIVE_SDK:-}" ]; then
echo "error: set OHOS_NATIVE_SDK to the OpenHarmony native SDK directory. It must contain llvm/bin and sysroot." >&2
exit 1
fi
sdk=$OHOS_NATIVE_SDK
clangxx=$sdk/llvm/bin/clang++
sysroot=$sdk/sysroot
if [ ! -x "$clangxx" ]; then
echo "error: OHOS_NATIVE_SDK does not contain executable llvm/bin/clang++: $sdk" >&2
exit 1
fi
if [ ! -d "$sysroot" ]; then
echo "error: OHOS_NATIVE_SDK does not contain sysroot: $sdk" >&2
exit 1
fi
exec "$clangxx" -target aarch64-linux-ohos "--sysroot=$sysroot" -D__MUSL__ "$@"
+57
View File
@@ -0,0 +1,57 @@
$ErrorActionPreference = "Stop"
function Stop-OhosEnv {
param([string]$Message)
[Console]::Error.WriteLine("error: $Message")
throw "OpenHarmony Cargo environment setup failed."
}
if ([string]::IsNullOrWhiteSpace($env:OHOS_NATIVE_SDK)) {
Stop-OhosEnv "set OHOS_NATIVE_SDK to the OpenHarmony native SDK directory."
}
if (-not (Test-Path -LiteralPath $env:OHOS_NATIVE_SDK -PathType Container -ErrorAction SilentlyContinue)) {
Stop-OhosEnv "OHOS_NATIVE_SDK does not exist: $env:OHOS_NATIVE_SDK"
}
$sdk = (Resolve-Path -LiteralPath $env:OHOS_NATIVE_SDK -ErrorAction Stop).Path
$clang = [System.IO.Path]::Combine($sdk, "llvm", "bin", "clang.exe")
$clangxx = [System.IO.Path]::Combine($sdk, "llvm", "bin", "clang++.exe")
$ar = [System.IO.Path]::Combine($sdk, "llvm", "bin", "llvm-ar.exe")
$sysroot = [System.IO.Path]::Combine($sdk, "sysroot")
$cmakeToolchain = [System.IO.Path]::Combine($sdk, "build", "cmake", "ohos.toolchain.cmake")
$requiredFiles = @($clang, $clangxx, $ar, $cmakeToolchain)
foreach ($path in $requiredFiles) {
if (-not (Test-Path -LiteralPath $path -PathType Leaf -ErrorAction SilentlyContinue)) {
Stop-OhosEnv "required OpenHarmony SDK file is missing: $path"
}
}
if (-not (Test-Path -LiteralPath $sysroot -PathType Container -ErrorAction SilentlyContinue)) {
Stop-OhosEnv "required OpenHarmony SDK sysroot is missing: $sysroot"
}
$target = "aarch64_unknown_linux_ohos"
$targetUpper = "AARCH64_UNKNOWN_LINUX_OHOS"
$commonFlags = "-target aarch64-linux-ohos --sysroot=`"$sysroot`" -D__MUSL__"
$env:CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_LINKER = $clang
$env:AR_aarch64_unknown_linux_ohos = $ar
$env:CC_aarch64_unknown_linux_ohos = $clang
$env:CXX_aarch64_unknown_linux_ohos = $clangxx
$env:CC_SHELL_ESCAPED_FLAGS = "1"
Set-Item -Path "Env:CFLAGS_$target" -Value $commonFlags
Set-Item -Path "Env:CXXFLAGS_$target" -Value $commonFlags
Set-Item -Path "Env:CMAKE_TOOLCHAIN_FILE_$target" -Value $cmakeToolchain
$separator = [char]0x1f
$env:CARGO_ENCODED_RUSTFLAGS = @(
"-Clink-arg=-target",
"-Clink-arg=aarch64-linux-ohos",
"-Clink-arg=--sysroot=$sysroot",
"-Clink-arg=-D__MUSL__"
) -join $separator
Write-Host "Configured OpenHarmony Cargo environment for $targetUpper from $sdk"
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env sh
if [ -z "${OHOS_NATIVE_SDK:-}" ]; then
echo "error: set OHOS_NATIVE_SDK to the OpenHarmony native SDK directory." >&2
return 1 2>/dev/null || exit 1
fi
if [ ! -d "$OHOS_NATIVE_SDK" ]; then
echo "error: OHOS_NATIVE_SDK does not exist: $OHOS_NATIVE_SDK" >&2
return 1 2>/dev/null || exit 1
fi
sdk=$(cd "$OHOS_NATIVE_SDK" && pwd)
clang=$sdk/llvm/bin/clang
clangxx=$sdk/llvm/bin/clang++
ar=$sdk/llvm/bin/llvm-ar
sysroot=$sdk/sysroot
cmake_toolchain=$sdk/build/cmake/ohos.toolchain.cmake
for file in "$clang" "$clangxx" "$ar" "$cmake_toolchain"; do
if [ ! -f "$file" ]; then
echo "error: required OpenHarmony SDK file is missing: $file" >&2
return 1 2>/dev/null || exit 1
fi
done
if [ ! -d "$sysroot" ]; then
echo "error: required OpenHarmony SDK sysroot is missing: $sysroot" >&2
return 1 2>/dev/null || exit 1
fi
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_LINKER=$clang
export AR_aarch64_unknown_linux_ohos=$ar
export CC_aarch64_unknown_linux_ohos=$clang
export CXX_aarch64_unknown_linux_ohos=$clangxx
export CC_SHELL_ESCAPED_FLAGS=1
export CFLAGS_aarch64_unknown_linux_ohos="-target aarch64-linux-ohos --sysroot=\"$sysroot\" -D__MUSL__"
export CXXFLAGS_aarch64_unknown_linux_ohos="-target aarch64-linux-ohos --sysroot=\"$sysroot\" -D__MUSL__"
export CMAKE_TOOLCHAIN_FILE_aarch64_unknown_linux_ohos=$cmake_toolchain
sep=$(printf '\037')
export CARGO_ENCODED_RUSTFLAGS="-Clink-arg=-target${sep}-Clink-arg=aarch64-linux-ohos${sep}-Clink-arg=--sysroot=$sysroot${sep}-Clink-arg=-D__MUSL__"
echo "Configured OpenHarmony Cargo environment for AARCH64_UNKNOWN_LINUX_OHOS from $sdk"