以 dust(磁盘使用分析工具)为例,详解如何将 Rust CLI 工具适配到 OpenHarmony/HarmonyOS 平台

前言

随着鸿蒙生态的快速发展,越来越多的开发者希望将现有的命令行工具移植到鸿蒙系统。本文以 dust(一个用 Rust 编写的磁盘使用分析工具)为例,详细介绍从环境配置到构建部署的完整流程,并记录适配过程中遇到的所有问题及解决方案。

原项目信息

项目 说明
项目名称 dust (du-dust)
项目描述 一个更直观的磁盘使用分析工具
技术栈 Rust
主要依赖 clap, rayon, terminal_size, lscolors

适配目标

将 dust 核心功能适配到 OpenHarmony/HarmonyOS 平台,支持两种使用方式:

  1. HNP 原生命令行:在鸿蒙终端直接使用
  2. NAPI 绑定:供 ArkTS/JS 应用调用

适用场景

  • 将 Rust CLI 工具移植到鸿蒙系统
  • 为鸿蒙应用提供原生 Rust 库支持
  • 了解 ohos-rs 框架的使用方法

一、开发环境配置

1.1 基础环境要求

工具 版本要求 用途
Rust 1.77+ 编程语言工具链
DevEco Studio NEXT Beta1 (5.0.3.100)+ 鸿蒙开发 IDE
OpenHarmony SDK API 12+ 交叉编译工具链

1.2 安装 Rust 工具链

# 安装 Rust(如果尚未安装)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 添加 OpenHarmony 目标平台
rustup target add aarch64-unknown-linux-ohos   # ARM64 设备
rustup target add armv7-unknown-linux-ohos     # ARM32 设备
rustup target add x86_64-unknown-linux-ohos    # x86_64 模拟器

# 验证安装
rustup target list | grep ohos

输出示例

aarch64-unknown-linux-ohos (installed)
armv7-unknown-linux-ohos (installed)
x86_64-unknown-linux-ohos (installed)

1.3 配置 OpenHarmony SDK

  1. 下载 DevEco Studio:从华为开发者官网下载并安装

  2. 安装 SDK:在 DevEco Studio 中,通过 Settings > SDK 下载 Native SDK

  3. 配置环境变量

# 添加到 ~/.bashrc 或 ~/.zshrc
export OHOS_NDK_HOME=/path/to/ohos-sdk/native
export PATH="$OHOS_NDK_HOME/llvm/bin:$PATH"

# 使配置生效
source ~/.bashrc
  1. 验证配置
echo $OHOS_NDK_HOME
which aarch64-unknown-linux-ohos-clang

1.4 创建 Cargo 交叉编译配置

在项目根目录创建 .cargo/config.toml

# .cargo/config.toml

[target.aarch64-unknown-linux-ohos]
linker = "aarch64-unknown-linux-ohos-clang"
ar = "llvm-ar"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

[target.armv7-unknown-linux-ohos]
linker = "armv7-unknown-linux-ohos-clang"
ar = "llvm-ar"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

[target.x86_64-unknown-linux-ohos]
linker = "x86_64-unknown-linux-ohos-clang"
ar = "llvm-ar"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

二、项目结构重构

2.1 为什么需要重构?

原始的 dust 项目是一个单体 CLI 应用,所有代码都在一个 crate 中。为了适配鸿蒙平台,我们需要:

  1. 代码复用:核心逻辑可被 CLI 和 NAPI 模块共享
  2. 平台隔离:平台特定代码集中管理
  3. 独立构建:CLI 和 NAPI 库可以独立构建

2.2 目标结构

dust/
├── Cargo.toml              # 工作空间配置
├── .cargo/
│   └── config.toml         # 交叉编译配置
├── scripts/
│   ├── build-hnp.sh        # HNP 构建脚本
│   └── build-ohos.sh       # NAPI 库构建脚本
├── dust-core/              # 核心库(平台无关)
│   ├── Cargo.toml
│   └── src/
│       ├── lib.rs          # 库入口
│       ├── node.rs         # 文件节点
│       ├── scanner.rs      # 扫描器 API
│       ├── types.rs        # 类型定义
│       ├── utils.rs        # 工具函数
│       ├── walker.rs       # 目录遍历
│       └── platform/       # 平台抽象层
│           ├── mod.rs
│           ├── unix.rs
│           ├── windows.rs
│           └── ohos.rs     # OpenHarmony 实现
├── dust-cli/               # CLI 应用
│   ├── Cargo.toml
│   ├── build.rs
│   └── src/
│       └── main.rs
└── dust-ohos/              # NAPI 绑定(可选)
    ├── Cargo.toml
    ├── build.rs
    ├── index.d.ts          # TypeScript 类型定义
    ├── examples/
    │   └── usage.ets       # ArkTS 使用示例
    └── src/
        ├── lib.rs          # NAPI 导出
        ├── types.rs        # JS 类型转换
        └── error.rs        # 错误处理

2.3 创建工作空间配置

根 Cargo.toml

[workspace]
resolver = "2"
members = [
    "dust-core",
    "dust-cli",
    "dust-ohos",
]

[workspace.package]
version = "1.2.4"
authors = ["bootandy <bootandy@gmail.com>"]
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/bootandy/dust"

[workspace.dependencies]
rayon = "1"
regex = "1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"

[profile.release]
codegen-units = 1
lto = true
strip = true

2.4 核心库配置

dust-core/Cargo.toml

[package]
name = "dust-core"
version.workspace = true
edition.workspace = true
license.workspace = true

[features]
default = []
ohos = []  # OpenHarmony 特性标志

[dependencies]
rayon.workspace = true
regex.workspace = true
serde.workspace = true

[dev-dependencies]
tempfile = "3"  # 测试依赖

关键点:通过 ohos feature flag 控制 OpenHarmony 特定代码的编译。


三、平台抽象层实现

3.1 设计思路

问题:原 platform.rs 使用 #[cfg(target_family = "unix")]#[cfg(target_family = "windows")],但 OpenHarmony 目标 (*-unknown-linux-ohos) 会匹配 target_family = "unix",导致无法区分普通 Unix 和 OpenHarmony。

解决方案:使用 Cargo feature flag 来控制条件编译:

// dust-core/src/platform/mod.rs

#[cfg(all(target_family = "unix", not(feature = "ohos")))]
mod unix;

#[cfg(target_family = "windows")]
mod windows;

#[cfg(feature = "ohos")]
mod ohos;

// 统一导出
#[cfg(all(target_family = "unix", not(feature = "ohos")))]
pub use unix::*;

#[cfg(target_family = "windows")]
pub use windows::*;

#[cfg(feature = "ohos")]
pub use ohos::*;

3.2 OpenHarmony 平台实现

OpenHarmony 基于 Linux 内核,可以复用大部分 Unix API:

// dust-core/src/platform/ohos.rs

use std::os::unix::fs::MetadataExt;
use std::path::Path;

/// 文件元数据
pub struct FileMetadata {
    pub size: u64,
    pub inode_device: Option<(u64, u64)>,
    pub file_time: (i64, i64, i64),  // mtime, atime, ctime
}

pub struct OhosPlatform;

impl OhosPlatform {
    pub fn get_metadata<P: AsRef<Path>>(
        path: P,
        use_apparent_size: bool,
        follow_links: bool,
    ) -> Option<FileMetadata> {
        let metadata = if follow_links {
            path.as_ref().metadata()
        } else {
            path.as_ref().symlink_metadata()
        };

        match metadata {
            Ok(md) => {
                let file_size = md.len();
                let size = if use_apparent_size {
                    file_size
                } else {
                    // 计算实际块大小
                    // OpenHarmony 使用标准 512 字节块
                    let blocks = md.blocks();
                    blocks * 512
                };

                Some(FileMetadata {
                    size,
                    inode_device: Some((md.ino(), md.dev())),
                    file_time: (md.mtime(), md.atime(), md.ctime()),
                })
            }
            Err(_) => None,
        }
    }
}

3.3 OpenHarmony 平台注意事项

在 OpenHarmony 上开发需要注意以下限制:

限制类型 说明 解决方案
沙箱限制 应用只能访问自己的沙箱目录 限制扫描范围到允许的路径
权限申请 访问外部存储需要申请权限 在 module.json5 中声明权限
路径差异 系统路径与标准 Linux 不同 使用正确的鸿蒙路径

常见可访问路径

/data/storage/el1/base/<bundle_name>/  - 应用私有目录
/data/storage/el2/base/<bundle_name>/  - 加密目录(需要解锁)
/data/storage/el2/base/<bundle_name>/cache/  - 缓存目录
/data/storage/el2/base/<bundle_name>/files/  - 文件目录

四、核心模块迁移

4.1 Node 模块迁移

问题:原代码中 node.rs 依赖 cli.rs 中的 FileTime 定义。

解决方案:在 dust-core 中重新定义 FileTime 枚举:

// dust-core/src/types.rs

/// 文件时间排序类型
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FileTime {
    Modified,
    Accessed,
    Changed,
}

impl Default for FileTime {
    fn default() -> Self {
        FileTime::Modified
    }
}

CLI 模块通过 From trait 进行转换:

// dust-cli/src/cli.rs

impl From<CliFileTime> for dust_core::FileTime {
    fn from(t: CliFileTime) -> Self {
        match t {
            CliFileTime::Modified => dust_core::FileTime::Modified,
            CliFileTime::Accessed => dust_core::FileTime::Accessed,
            CliFileTime::Changed => dust_core::FileTime::Changed,
        }
    }
}

4.2 Walker 模块迁移

问题:原 dir_walker.rs 硬编码了终端进度指示器,无法在 NAPI 环境中使用。

解决方案

  1. 重命名为 walker.rs
  2. 设计可选的进度回调接口
// dust-core/src/walker.rs

/// 进度回调 trait
pub trait ProgressCallback: Send + Sync {
    fn on_progress(&self, current: usize, total: Option<usize>);
    fn on_error(&self, path: &Path, error: &str);
}

/// 空实现(用于不需要进度的场景)
pub struct NoProgress;

impl ProgressCallback for NoProgress {
    fn on_progress(&self, _current: usize, _total: Option<usize>) {}
    fn on_error(&self, _path: &Path, _error: &str) {}
}

/// 扫描配置
pub struct WalkConfig {
    pub progress: Option<Box<dyn ProgressCallback>>,
    pub threads: usize,
    pub follow_links: bool,
    // ...
}

4.3 Scanner API 设计

为 NAPI 绑定提供简洁的 API:

// dust-core/src/scanner.rs

use crate::{WalkConfig, FileTime};
use std::path::PathBuf;

/// 扫描结果
pub struct ScanResult {
    pub total_size: u64,
    pub file_count: u64,
    pub dir_count: u64,
    pub nodes: Vec<FileNode>,
    pub errors: ScanErrors,
}

/// 扫描错误信息
pub struct ScanErrors {
    pub no_permissions: Vec<PathBuf>,
    pub not_found: Vec<PathBuf>,
    pub other: Vec<(PathBuf, String)>,
}

/// 同步扫描(阻塞)
pub fn scan_sync(paths: Vec<PathBuf>, config: ScanConfig) -> ScanResult {
    // 实现扫描逻辑
}

/// 快速获取目录大小
pub fn get_directory_size(path: &Path) -> u64 {
    // 快速计算,不构建完整树
}

/// 获取文件数量
pub fn get_file_count(path: &Path) -> u64 {
    // 快速计数
}

五、NAPI 绑定层实现

5.1 NAPI 库配置

dust-ohos/Cargo.toml

[package]
name = "dust-ohos"
version.workspace = true
edition.workspace = true

[lib]
crate-type = ["cdylib"]  # 编译为动态库

[dependencies]
dust-core = { path = "../dust-core", features = ["ohos"] }
napi-ohos = "1"
napi-derive-ohos = "1"
serde = { workspace = true }
serde_json = { workspace = true }

[build-dependencies]
napi-build-ohos = "1"

5.2 build.rs 配置

// dust-ohos/build.rs

extern crate napi_build_ohos;

fn main() {
    napi_build_ohos::setup();
}

5.3 NAPI 导出函数

// dust-ohos/src/lib.rs

use napi_derive_ohos::napi;
use dust_core::{scan_sync, ScanConfig};
use std::path::PathBuf;

mod types;
mod error;

use types::*;

/// 扫描单个目录
#[napi]
pub fn scan_directory(
    path: String,
    options: Option<ScanOptionsJs>
) -> napi_ohos::Result<ScanResultJs> {
    let config = options
        .map(ScanConfig::from)
        .unwrap_or_default();

    let paths = vec![PathBuf::from(path)];
    let result = scan_sync(paths, config);

    Ok(ScanResultJs::from(result))
}

/// 扫描多个目录
#[napi]
pub fn scan_directories(
    paths: Vec<String>,
    options: Option<ScanOptionsJs>
) -> napi_ohos::Result<ScanResultJs> {
    let config = options
        .map(ScanConfig::from)
        .unwrap_or_default();

    let paths: Vec<PathBuf> = paths.into_iter()
        .map(PathBuf::from)
        .collect();

    let result = scan_sync(paths, config);

    Ok(ScanResultJs::from(result))
}

/// 快速获取目录大小
#[napi]
pub fn get_directory_size(path: String) -> i64 {
    dust_core::get_directory_size(&PathBuf::from(path)) as i64
}

/// 获取文件数量
#[napi]
pub fn get_file_count(path: String) -> i64 {
    dust_core::get_file_count(&PathBuf::from(path)) as i64
}

/// 格式化大小显示
#[napi]
pub fn format_size(size: i64, format: Option<String>) -> String {
    let fmt = format.as_deref().unwrap_or("");
    dust_core::utils::human_readable_number(size as u64, fmt)
}

/// 检查路径是否可访问
#[napi]
pub fn is_path_accessible(path: String) -> bool {
    std::fs::metadata(&path).is_ok()
}

/// 获取版本信息
#[napi]
pub fn get_version() -> String {
    env!("CARGO_PKG_VERSION").to_string()
}

5.4 TypeScript 类型定义

dust-ohos/index.d.ts

/* auto-generated by NAPI-RS */

export interface ScanOptions {
  /** 是否忽略隐藏文件 */
  ignoreHidden?: boolean;
  /** 最大扫描深度 */
  maxDepth?: number;
  /** 是否跟踪符号链接 */
  followLinks?: boolean;
  /** 使用的线程数 */
  threads?: number;
  /** 过滤正则表达式 */
  filterRegex?: string[];
  /** 忽略的目录名 */
  ignoreDirectories?: string[];
  /** 按文件数量而非大小排序 */
  byFilecount?: boolean;
}

export interface FileNode {
  /** 文件/目录名 */
  name: string;
  /** 完整路径 */
  path: string;
  /** 大小(字节)或文件数量 */
  size: number;
  /** 是否为目录 */
  isDirectory: boolean;
  /** 子节点 */
  children: FileNode[];
}

export interface ScanErrors {
  /** 无权限访问的路径 */
  noPermissions: string[];
  /** 未找到的路径 */
  notFound: string[];
  /** 其他错误 */
  other: Array<{ path: string; error: string }>;
}

export interface ScanResult {
  /** 总大小(字节) */
  totalSize: number;
  /** 文件数量 */
  fileCount: number;
  /** 目录数量 */
  dirCount: number;
  /** 文件树 */
  nodes: FileNode[];
  /** 错误信息 */
  errors: ScanErrors;
}

/** 扫描单个目录 */
export function scanDirectory(
  path: string,
  options?: ScanOptions
): ScanResult;

/** 扫描多个目录 */
export function scanDirectories(
  paths: string[],
  options?: ScanOptions
): ScanResult;

/** 快速获取目录大小 */
export function getDirectorySize(path: string): number;

/** 获取文件数量 */
export function getFileCount(path: string): number;

/** 格式化大小显示 */
export function formatSize(size: number, format?: string): string;

/** 检查路径是否可访问 */
export function isPathAccessible(path: string): boolean;

/** 获取版本信息 */
export function getVersion(): string;

六、构建脚本编写

6.1 HNP 构建脚本

HNP (HarmonyOS Native Package) 是鸿蒙系统的原生包格式,用于分发命令行工具。

scripts/build-hnp.sh

#!/bin/bash
# OpenHarmony HNP 构建脚本
# 用法: ./scripts/build-hnp.sh [arch]
# arch: aarch64, armv7, x86_64, all (默认)

set -e

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
echo_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
echo_error() { echo -e "${RED}[ERROR]${NC} $1"; }

# 项目信息
PKG_NAME="dust"
PKG_VERSION="1.2.4"
PKG_DESCRIPTION="A more intuitive version of du - disk usage analyzer"

# 检查环境变量
check_environment() {
    if [ -z "$OHOS_NDK_HOME" ]; then
        echo_error "OHOS_NDK_HOME is not set"
        echo "Please set OHOS_NDK_HOME to your OpenHarmony NDK path"
        echo "Example: export OHOS_NDK_HOME=/path/to/ohos-sdk/native"
        exit 1
    fi

    echo_info "OHOS_NDK_HOME: $OHOS_NDK_HOME"

    if [ ! -d "$OHOS_NDK_HOME" ]; then
        echo_error "OHOS_NDK_HOME path does not exist: $OHOS_NDK_HOME"
        exit 1
    fi
}

# 获取架构名称映射
get_arch_name() {
    local target=$1
    case "$target" in
        aarch64-unknown-linux-ohos) echo "arm64-v8a" ;;
        armv7-unknown-linux-ohos) echo "armeabi-v7a" ;;
        x86_64-unknown-linux-ohos) echo "x86_64" ;;
    esac
}

# 构建函数
build_target() {
    local target=$1
    echo_info "Building CLI for $target..."

    export PATH="$OHOS_NDK_HOME/llvm/bin:$PATH"
    cargo build --release --target "$target" -p du-dust

    if [ $? -eq 0 ]; then
        echo_info "Build successful for $target"
    else
        echo_error "Build failed for $target"
        return 1
    fi
}

# 创建 HNP 包
create_hnp() {
    local target=$1
    local arch=$(get_arch_name "$target")
    local hnp_dir="dist/hnp/$arch"
    local bin_dir="$hnp_dir/bin"

    echo_info "Creating HNP package for $arch..."

    mkdir -p "$bin_dir"

    # 复制可执行文件
    local exe_path="target/$target/release/dust"
    if [ -f "$exe_path" ]; then
        cp "$exe_path" "$bin_dir/"
        chmod +x "$bin_dir/dust"
    else
        echo_error "Executable not found: $exe_path"
        return 1
    fi

    # 创建 hnp.json 配置文件
    cat > "$hnp_dir/hnp.json" << EOF
{
    "name": "$PKG_NAME",
    "version": "$PKG_VERSION",
    "description": "$PKG_DESCRIPTION",
    "type": "hnp",
    "install": {
        "links": [
            {
                "source": "bin/dust",
                "target": "dust"
            }
        ]
    }
}
EOF

    # 打包 HNP
    local hnp_file="dist/${PKG_NAME}_${PKG_VERSION}_${arch}.hnp"
    (cd "$hnp_dir" && zip -r "../../${PKG_NAME}_${PKG_VERSION}_${arch}.hnp" . -x "*.DS_Store")
    echo_info "Created HNP package: $hnp_file"
}

# 主函数
main() {
    local arch="${1:-all}"

    echo_info "=== OpenHarmony HNP Build Script ==="
    echo_info "Building $PKG_NAME v$PKG_VERSION"

    check_environment

    # 安装 Rust 目标
    rustup target add aarch64-unknown-linux-ohos 2>/dev/null || true
    rustup target add armv7-unknown-linux-ohos 2>/dev/null || true
    rustup target add x86_64-unknown-linux-ohos 2>/dev/null || true

    rm -rf dist/hnp

    case "$arch" in
        aarch64)
            build_target "aarch64-unknown-linux-ohos"
            create_hnp "aarch64-unknown-linux-ohos"
            ;;
        armv7)
            build_target "armv7-unknown-linux-ohos"
            create_hnp "armv7-unknown-linux-ohos"
            ;;
        x86_64)
            build_target "x86_64-unknown-linux-ohos"
            create_hnp "x86_64-unknown-linux-ohos"
            ;;
        all)
            for target in aarch64-unknown-linux-ohos armv7-unknown-linux-ohos x86_64-unknown-linux-ohos; do
                build_target "$target"
                create_hnp "$target"
            done
            ;;
        *)
            echo_error "Unknown architecture: $arch"
            echo "Usage: $0 [aarch64|armv7|x86_64|all]"
            exit 1
            ;;
    esac

    echo_info "=== Build Complete ==="
    echo ""
    echo "HNP packages created in 'dist/' directory:"
    ls -la dist/*.hnp 2>/dev/null || echo "No HNP files found"
}

main "$@"

6.2 NAPI 库构建脚本

scripts/build-ohos.sh

#!/bin/bash
# OpenHarmony NAPI 库构建脚本

set -e

PKG_NAME="dust_ohos"

check_environment() {
    if [ -z "$OHOS_NDK_HOME" ]; then
        echo "Error: OHOS_NDK_HOME is not set"
        exit 1
    fi
}

build_target() {
    local target=$1
    local arch=$2

    echo "Building NAPI library for $target..."

    export PATH="$OHOS_NDK_HOME/llvm/bin:$PATH"
    cargo build --release --target "$target" -p dust-ohos

    mkdir -p "dist/$arch"
    cp "target/$target/release/lib${PKG_NAME}.so" "dist/$arch/"
    echo "Created: dist/$arch/lib${PKG_NAME}.so"
}

main() {
    check_environment

    rm -rf dist

    build_target "aarch64-unknown-linux-ohos" "arm64-v8a"
    build_target "armv7-unknown-linux-ohos" "armeabi-v7a"
    build_target "x86_64-unknown-linux-ohos" "x86_64"

    # 复制类型定义
    cp dust-ohos/index.d.ts dist/

    echo "Build complete! Output in 'dist/' directory"
}

main "$@"

6.3 执行构建

# 构建 HNP 包(命令行工具)
chmod +x scripts/build-hnp.sh
./scripts/build-hnp.sh

# 构建 NAPI 库(可选)
chmod +x scripts/build-ohos.sh
./scripts/build-ohos.sh

构建输出示例

[INFO] === OpenHarmony HNP Build Script ===
[INFO] Building dust v1.2.4
[INFO] OHOS_NDK_HOME: /Users/xxx/Library/OpenHarmony/Sdk/12/native
[INFO] Building CLI for aarch64-unknown-linux-ohos...
   Compiling dust-core v1.2.4
   Compiling du-dust v1.2.4
    Finished release [optimized] target(s) in 45.23s
[INFO] Build successful for aarch64-unknown-linux-ohos
[INFO] Creating HNP package for arm64-v8a...
[INFO] Created HNP package: dist/dust_1.2.4_arm64-v8a.hnp
[INFO] === Build Complete ===

HNP packages created in 'dist/' directory:
-rw-r--r--  1 user  staff  1.2M Jan 26 10:30 dust_1.2.4_arm64-v8a.hnp

七、部署与测试

7.1 连接设备

# 查看已连接设备
hdc list targets

# 输出示例
# 1234567890ABCDEF    device

7.2 安装 HNP 包

# 发送 HNP 包到设备
hdc file send dist/dust_1.2.4_arm64-v8a.hnp /data/local/tmp/

# 安装 HNP 包
hdc shell hnp install /data/local/tmp/dust_1.2.4_arm64-v8a.hnp

7.3 运行测试

# 进入设备 shell
hdc shell

# 运行 dust
/data/app/bin/dust /data

# 示例输出
#  1.2G ┌── storage
#  800M │   ├── el2
#  400M │   │   └── base
#  400M │   └── el1
# ...

7.4 CLI 命令参考

# 基本用法
dust [目录路径]

# 常用选项
dust -d 3 /data           # 限制深度为 3
dust -r /data             # 反向排序
dust -n 20 /data          # 显示前 20 个
dust -H /data             # 忽略隐藏文件
dust -c /data             # 按文件数量排序
dust -j /data             # JSON 输出
dust -s /data             # 使用 SI 单位(KB, MB)
dust -b /data             # 使用二进制单位(KiB, MiB)

八、ArkTS 使用示例(NAPI 方式)

如果选择使用 NAPI 绑定,以下是在 ArkTS 中的使用示例:

// 导入 NAPI 库
import {
  scanDirectory,
  scanDirectories,
  getDirectorySize,
  formatSize,
  getVersion,
  getFileCount,
  isPathAccessible,
  ScanOptions,
  ScanResult,
  FileNode
} from 'libdust_ohos.so';

// 示例 1: 扫描单个目录
function scanSingleDirectory(): void {
  const path = '/data/storage/el2/base';

  if (isPathAccessible(path)) {
    const result: ScanResult = scanDirectory(path, {
      ignoreHidden: true,
      maxDepth: 5
    });

    console.log(`Total size: ${formatSize(result.totalSize)}`);
    console.log(`File count: ${result.fileCount}`);

    // 打印错误信息
    if (result.errors.noPermissions.length > 0) {
      console.log(`Permission denied for: ${result.errors.noPermissions.join(', ')}`);
    }
  }
}

// 示例 2: 扫描多个目录
function scanMultipleDirectories(): void {
  const paths = [
    '/data/storage/el1/base',
    '/data/storage/el2/base'
  ];

  const result = scanDirectories(paths, {
    followLinks: false,
    threads: 4
  });

  console.log(`Combined size: ${formatSize(result.totalSize)}`);
}

// 示例 3: 快速获取目录大小
function getQuickSize(): void {
  const cacheDir = '/data/storage/el2/base/cache';
  const size = getDirectorySize(cacheDir);
  console.log(`Cache directory size: ${formatSize(size)}`);
}

// 示例 4: 遍历文件树
function traverseFileTree(node: FileNode, indent: string = ''): void {
  const sizeStr = formatSize(node.size);
  console.log(`${indent}${node.name} (${sizeStr})`);

  for (const child of node.children) {
    traverseFileTree(child, indent + '  ');
  }
}

// 示例 5: 查找大文件(>10MB)
function findLargeFiles(nodes: FileNode[], minSize: number = 10 * 1024 * 1024): FileNode[] {
  const largeFiles: FileNode[] = [];

  function traverse(node: FileNode): void {
    if (!node.isDirectory && node.size >= minSize) {
      largeFiles.push(node);
    }
    for (const child of node.children) {
      traverse(child);
    }
  }

  for (const node of nodes) {
    traverse(node);
  }

  return largeFiles.sort((a, b) => b.size - a.size);
}

// 示例 6: 使用过滤器
function scanWithFilters(): void {
  const result = scanDirectory('/data/storage/el2/base', {
    // 只扫描 .log 文件
    filterRegex: ['\\.log$'],
    // 忽略 node_modules 目录
    ignoreDirectories: ['node_modules', '.git'],
    // 忽略隐藏文件
    ignoreHidden: true
  });

  console.log(`Log files total: ${formatSize(result.totalSize)}`);
}

九、遇到的问题与解决方案

问题 1:找不到链接器

错误信息

error: linker `aarch64-unknown-linux-ohos-clang` not found

原因:OHOS NDK 的 llvm/bin 目录未添加到 PATH。

解决方案

# 确保 NDK 路径正确
echo $OHOS_NDK_HOME
ls $OHOS_NDK_HOME/llvm/bin/

# 将 llvm/bin 添加到 PATH
export PATH="$OHOS_NDK_HOME/llvm/bin:$PATH"

问题 2:平台条件编译冲突

问题描述:OpenHarmony 目标 (*-unknown-linux-ohos) 会匹配 target_family = "unix",导致无法使用标准的条件编译区分平台。

解决方案:使用 Cargo feature flag 区分:

# Cargo.toml
[features]
ohos = []
// 条件编译
#[cfg(all(target_family = "unix", not(feature = "ohos")))]
mod unix;

#[cfg(feature = "ohos")]
mod ohos;
# 构建时启用 feature
cargo build --features ohos --target aarch64-unknown-linux-ohos

问题 3:CLI 与核心库耦合

问题描述:原代码中 node.rs 依赖 cli.rs 中的 FileTime 定义,无法分离为独立的核心库。

解决方案:在 dust-core 中重新定义 FileTime 枚举,CLI 模块通过 From trait 转换。

问题 4:进度指示器依赖

问题描述:原 dir_walker.rs 硬编码了终端进度指示器(使用 indicatif crate),无法在 NAPI 环境中使用。

解决方案:设计可选的进度回调接口:

pub trait ProgressCallback: Send + Sync {
    fn on_progress(&self, current: usize, total: Option<usize>);
}

CLI 和 NAPI 可以提供不同的实现,或使用空实现。

问题 5:tempfile 测试依赖缺失

问题描述scanner.rs 中的测试使用了 tempfile crate,但未在 Cargo.toml 中声明。

错误信息

error[E0432]: unresolved import `tempfile`

解决方案:在 dust-core/Cargo.toml 中添加开发依赖:

[dev-dependencies]
tempfile = "3"

问题 6:未使用的错误处理函数警告

问题描述error.rs 中定义的辅助函数(如 to_napi_errorpermission_error)未被使用,产生编译警告。

解决方案:添加属性允许死代码(这些函数是为将来的错误处理预留的):

#![allow(dead_code)]

问题 7:build.rs 路径问题

问题描述:原始 build.rs 使用相对路径生成 completions 和 man-page,在工作空间结构下路径不正确,导致文件生成到错误的位置。

解决方案:使用 CARGO_MANIFEST_DIR 环境变量获取正确的 crate 根目录路径:

// build.rs
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
    .unwrap_or_else(|_| ".".to_string());
let completions_dir = Path::new(&manifest_dir).join("completions");
let man_dir = Path::new(&manifest_dir).join("man-page");

问题 8:权限不足

问题描述:在设备上运行时无法访问某些目录。

解决方案

  • 只扫描应用沙箱内的目录
  • module.json5 中申请必要权限:
{
  "requestPermissions": [
    {
      "name": "ohos.permission.READ_MEDIA"
    }
  ]
}

问题 9:找不到共享库

错误信息

error while loading shared libraries: libxxx.so: cannot open shared object file

解决方案:确保使用静态链接,在 Cargo.toml 中配置:

[profile.release]
lto = true  # 链接时优化,有助于减少外部依赖

十、总结

本文以 dust 项目为例,完整演示了 Rust CLI 工具的鸿蒙化适配流程:

阶段 关键步骤 主要产出
环境配置 安装 Rust 目标、配置 SDK 路径 .cargo/config.toml
项目重构 工作空间结构、模块分离 dust-core, dust-cli, dust-ohos
平台适配 平台抽象层、feature flag platform/ohos.rs
构建打包 HNP 构建脚本、交叉编译 .hnp
部署测试 HDC 工具安装、功能验证 设备上运行成功

核心要点

  1. 使用 feature flag 区分 Unix 和 OpenHarmony:因为 *-unknown-linux-ohos 目标会匹配 target_family = "unix"
  2. OpenHarmony 基于 Linux,可复用 Unix APIstd::os::unix::fs::MetadataExt 等接口可以直接使用
  3. 注意沙箱限制和权限管理:应用只能访问自己的沙箱目录,访问外部存储需要申请权限
  4. HNP 是原生命令行工具的标准打包格式:包含 hnp.json 配置文件和可执行文件
  5. NAPI 绑定可选:如果只需要命令行工具,可以跳过 NAPI 绑定部分

构建方式选择

方式 用途 输出 构建脚本
HNP 原生命令行 在鸿蒙终端直接使用 .hnp build-hnp.sh
NAPI 绑定 供 ArkTS/JS 调用 .so build-ohos.sh

参考资源


本文基于 dust v1.2.4 的实际适配经验编写。适配过程中共遇到并解决了 9 个主要问题,从项目结构重构到平台抽象层设计,再到构建脚本编写,完整记录了整个鸿蒙化适配流程。

欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐