A Rust reimplementation of the ohpm (OpenHarmony package manager) command-line tool.
The headline feature: ohpm-rs publish authenticates entirely from environment
variables — no TUI prompts. This makes CI/CD publishing unattended, which the
original Node implementation cannot do (it prompts for the private-key
passphrase interactively).
ohpm-rs (OpenHarmony package manager), a Rust reimplementation of ohpm
Commands:
install Install package(s) from the registry or local sources
update Update package(s) to their latest version
uninstall Uninstall package(s)
publish Publish a package to the registry
prepublish Pre-verify package content without publishing
init Create an oh-package.json5 file
config Manage the ohpm configuration file
login Log in with a private key and store the access token
unpublish Unpublish a package from the registry
info Display information about a package
list Display the dependency graph (basic)
ping Test network connectivity to the registry
root Print the effective oh_modules folder
version Bump the package version
cache Manage the ohpm cache folder
A cargo workspace with two crates:
crates/ohpm-core— library: configuration (.ohpmrc+ env overrides), package manifest/archive handling, integrity hashing (sha1 + sha512 in ssri format), the registry client, RSA-signature login, the publish orchestrator, and the install pipeline. Fully unit- and integration-tested against a mock registry.crates/ohpm-core/src/install/— the install subsystem (mirrorslib/core/install/, architected like pnpm's Rust engine): dependency resolution (spec/semver/packument/resolver), theoh-package-lock.json5lockfile (lockfile), the dependency graph (graph/node), the content-addressed store + extraction (store), theoh_modulessymlink phase (symlink), theoh_modules/.ohpm/lock.json5install record (lock_record) and the pipeline orchestration (root/mod). The lockfile, install record,oh_moduleslayout and manifest rewrites are byte-compatible with the reference (verified against ohpm 6.0.1 on the public registry).install,updateanduninstallshare the same pipeline (run_pipeline), like the reference'sinstallModules;updateclears/deletes the matching lockfile specifiers before re-resolving, anduninstalldrops the packages from the root requirements and rewrites the manifest.
Version conflicts are resolved to the max-satisfying version by default
(resolveVersionConflict2LockFile + the flattened graph view), the lockfile
specifiers/packages are rewritten accordingly, and the strict strategy
(resolve_conflict_strict) is implemented including the strict alarms; the
local dependency-name inconsistency alarm (enforced by
enforce_dependency_key / the build-profile useNormalizedOHMUrl config) and
the registry name case-consistency alarm mirror lib/core/alarm/. The
overrides / overrideDependencyMap / exclusions fields of
oh-package.json5 are applied during the graph build (mask + exclusion, the
maskedByOverrideDependencyMap tag written to the lockfile and the install
record), parameterized installs (@param: markers via --parameter-file or
the manifest parameterFile field) substitute the project manifest, and
target installs (--target_path + dependencyMap.json5) switch the module
roots, the lockfile name (oh-package-<target>-lock.json5) and write the
resolved module manifests into <target>/resolve-conflict/<module>.
Protocol extensions beyond the reference (which rejects them), mirroring pnpm/pacquet:
- git specs (
install/git.rs):git+https://..., scp-style andhttps://...gitURLs with#commit|branch|tag|semver:<range>|path:<dir>fragments; the commit is pinned in the lockfile and re-installs work with the fixture repo deleted. Pure-Rust via the gix crate (no system git). ohpm:aliases:"foo": "ohpm:bar@^1.0.0"installs bar under the alias key foo; the specifier keeps the alias, the packages key and the store dir use the real name.workspace:protocol:workspace:*|^|~|<range>|./path|<member>@*resolves againstohpm-workspace.yamlmembers as links; the publish flow rewrites them to versions (^/~prefixes kept, alias form becomesohpm:<member>@<version>).crates/ohpm-cli— theohpm-rsbinary: clap command definitions and thin command handlers.
Reference layout: the ohpm source in lib/ of the DevEco Studio 6.1.1.280
installation (config/, core/registry/, core/publish/, core/package/).
cargo build --workspace # binary: target/debug/ohpm-rs
cargo test --workspace # unit, mock-registry, and real-CLI testsThe source build supports Linux, Windows, and macOS. Package directory links use relative symlinks on Unix and NTFS junctions on Windows; junctions do not require administrator privileges or Developer Mode. File links in archives and Git dependencies are restored after their targets, with a hard-link fallback on Windows when symbolic-link privileges are unavailable.
GitHub Actions runs the complete workspace test suite and a release CLI build on Ubuntu, Windows, and macOS. CLI integration tests execute real installs, reinstalls, workspace retargeting, cleanup, local HAR installs, packing, prepublish checks, and unified version changes under paths containing spaces and Unicode. Registry tests use local mock servers and need no publishing credentials.
All auth inputs are read from OHPM_* environment variables, which override
.ohpmrc files. Precedence: CLI flags > env vars > .ohpmrc > defaults.
| Variable | Purpose |
|---|---|
OHPM_ACCESS_TOKEN |
Read-write access token — used directly, skips login. Highest priority. |
OHPM_PUBLISH_ID |
Publish id for the SSH-key login flow. |
OHPM_KEY_PATH |
Path to the encrypted private key for the login flow. |
OHPM_KEY_CONTENT |
The private key PEM content directly — no file needed (CI secrets). Alternative to OHPM_KEY_PATH. |
Key formats: PKCS#8 encrypted (BEGIN ENCRYPTED PRIVATE KEY), unencrypted
PKCS#8/PKCS#1, and traditional OpenSSL encrypted PKCS#1 (BEGIN RSA PRIVATE KEY with Proc-Type: 4,ENCRYPTED / DEK-Info: — AES-128/192/256-CBC,
DES-EDE3-CBC, DES-CBC), all decrypted in pure Rust.
| OHPM_KEY_PASSPHRASE | Private-key passphrase. No interactive prompt is ever shown. |
| OHPM_READ_ACCESS_TOKEN | Read-only token (used by info / ping). |
| OHPM_REGISTRY | Default registry override. |
| OHPM_PUBLISH_REGISTRY | Publish registry override. |
| OHPM_STRICT_SSL, OHPM_CA_FILES, OHPM_HTTP(S)_PROXY, OHPM_NO_PROXY, OHPM_LOG_LEVEL, OHPM_FETCH_TIMEOUT | Network / logging overrides. |
The SSH-login values can also be passed as CLI flags: publish, login and
unpublish accept --publish_id, --key_path (or --key_content for the
PEM text itself), and --passphrase (prefer the OHPM_* env vars in CI —
command-line arguments are visible in the process list).
OHPM_ACCESS_TOKEN{registry}:_authstored in.ohpmrc(e.g."//repo.harmonyos.com/ohpm/:_auth")- SSH-key login:
OHPM_PUBLISH_ID+OHPM_KEY_PATH+OHPM_KEY_PASSPHRASE→ RSA-PSS signature overv1-{publishId}-{timestamp}-{nonce}→POST {registry}login_pss→ access token. Falls back to the defaultloginendpoint on HTTP 400/404, exactly like the reference.
If a value (or the passphrase) is missing, publish fails fast with a clear error instead of prompting — the key behavioral difference from the original ohpm.
# Fast path: you already have a token. Publish from source by default —
# no argument packs the current package directory automatically.
export OHPM_ACCESS_TOKEN="<token>"
ohpm-rs publish --publish_registry https://repo.example.com/ohpm/
# Tokenless path: private-key login, fully from env.
export OHPM_PUBLISH_ID="<publish-id>"
export OHPM_KEY_PATH="/path/to/key.pem" # or OHPM_KEY_CONTENT="<pem>"
export OHPM_KEY_PASSPHRASE="<passphrase>"
ohpm-rs publish
# Pre-built packages: pass the har/tgz explicitly (or a source directory).
ohpm-rs publish my-lib.har
ohpm-rs publish ./src/mylib
# Obtain and persist a token once (stored in ~/.ohpm/.ohpmrc).
ohpm-rs login --publish_id <id> --key_path /path/to/key.pempublish (and prepublish) input resolution: no argument = current package
directory, packed on the fly; a directory argument = pack that directory; a
.har/.tgz argument = publish the pre-built package as-is. In a workspace,
--workspace publishes every publishable member (or --filter <pkgs> a
selected subset); publish: false members are skipped. Before each real
upload, publish queries the target publish registry for the exact
name@version. An already-published version is skipped with a warning, while
other versions of the same package continue normally. Registry/auth/protocol
errors fail the command instead of being treated as an unpublished version.
publish --dry-run validates everything locally — packing, metadata, and the
auth configuration (the key is parsed, the token/env/config inputs are
checked) — but makes no network requests and uploads nothing:
ohpm-rs publish --dry-run # [DRY RUN] +name version (size, files, auth)
ohpm-rs publish --workspace --dry-runohpm-rs pack builds a <name>-<version>.har from a source directory
(npm-pack style):
# Pack the current package directory (nearest oh-package.json5).
ohpm-rs pack
# Pack a specific module; --output controls the destination dir.
ohpm-rs pack native_ability --output dist/
# Source directory → har → publish in one step.
ohpm-rs publish native_ability --publish_registry https://repo.example.com/ohpm/Packaging rules (hvigor conventions):
- gzip tar with a
package/prefix; entries sorted for deterministic output. - Always excluded:
oh_modules,node_modules,.hvigor,.git,.idea,.ohpm,.tmp,.cxx,build,target,.DS_Store, nested*.har,oh-package-lock.json5. - A gitignore-style
.ohpmignoreat the module root filters out additional files (#comments,!negation,* ? **globs; a pattern without/also matches the file name at any depth). publish/prepublishaccept a directory input and pack it first.
A monorepo is identified by an ohpm-workspace.yaml at the workspace root
(schema modeled on pnpm-workspace.yaml):
packages:
- "packages/*"
- "modules/**"
exclude: # optional: drop members by path glob or package name
- "packages/internal"
- "@scope/private"
version: # optional versioning policy
mode: unified # unified | independent (default independent)When publishing a package that lives inside a workspace, file: protocol
dependencies are resolved and rewritten to the target package's version
before the metadata is uploaded, so published manifests never reference local
paths:
// source oh-package.json5
{
name: "@app/app",
version: "0.9.0",
dependencies: { "@demo/lib": "file:../lib" },
}// uploaded metadata dependencies
{ "@demo/lib": "1.2.0" }Rules:
- A
file:spec matches/^file:(\s+)?/i; the target path is resolved relative to the published package's source root (~expands to$HOME). - Targets may be a workspace member directory or any local package with an
oh-package.json5(also.har/.tgzarchives). - Unresolvable
file:entries independenciesfail the publish; the same indevDependenciesis kept with a warning. - Outside a workspace (no
ohpm-workspace.yaml), no rewriting happens.
| Command | Workspace mode |
|---|---|
install |
--workspace installs every member, including publish: false; --filter <pkgs> selects members by exact name; combine with --all to install their build-profile modules |
publish / prepublish |
file: deps rewritten to member versions before upload; accepts a member directory (auto-pack); --workspace/--filter publish every (selected) publishable member |
pack |
--workspace packs every publishable member; --filter <pkgs> selects; publish: false skipped |
list |
-r/--recursive lists every member's graph; at the workspace root all members are listed by default |
version |
--workspace unified, --filter, --preid, version.mode from the yaml, publish: false skipped |
unpublish, info |
operate by package name — work from anywhere |
init, config, login, ping, root, cache |
global / cwd-scoped — not workspace-scoped |
ohpm-rs install --workspace works from the workspace root or any member; the
root does not need an oh-package.json5. --prefix <dir> locates the workspace
from another directory. Membership and exclusions come from ohpm-workspace.yaml.
An unknown filter or missing workspace fails before installing any member, and a
member installation failure makes the command fail. Named package arguments and
--target_path cannot be combined with --workspace/--filter.
ohpm-rs install --workspace
ohpm-rs install --filter @demo/app,@demo/lib
ohpm-rs install --workspace --all --prefix ./my-workspaceohpm-rs version supports two workspace modes, selectable by the --workspace flag or
by version.mode in ohpm-workspace.yaml:
# Unified: every workspace member is set to the same new version
# (base = the workspace root's own version, else the highest member version).
ohpm-rs version --workspace minor
# Same effect when the workspace config declares version.mode: unified.
ohpm-rs version patch # run from the workspace root
# Filter to specific packages (comma-separated, repeatable):
ohpm-rs version --workspace patch --filter @demo/a,@demo/b
# Independent: only the package containing the current directory is bumped,
# works inside a workspace member too.
cd packages/a && ohpm-rs version patchPre-release versions (beta / rc / alpha) follow npm semver:
ohpm-rs version prerelease --preid beta # 1.0.0 -> 1.0.1-beta.0
ohpm-rs version prerelease --preid beta # 1.0.1-beta.0 -> 1.0.1-beta.1
ohpm-rs version prerelease --preid rc # 1.0.1-beta.1 -> 1.0.1-rc.0 (reset to rc)
ohpm-rs version prepatch --preid rc # 1.0.0 -> 1.0.1-rc.0
ohpm-rs version premajor --preid beta # 1.0.0 -> 2.0.0-beta.0
ohpm-rs version patch # 1.0.1-rc.0 -> 1.0.2 (drops pre-release)
ohpm-rs version 2.0.0-rc.1 # set an exact pre-release version--preid must match [0-9A-Za-z-]+ (no leading zeros when numeric). The
unified-mode base version compares full semver (a release outranks a
pre-release of the same number).
A package whose oh-package.json5 sets "publish": false is not
publishable:
- skipped by
ohpm-rs version --workspace(and the unified mode), - excluded from the unified base version,
- an explicit
ohpm-rs publish <file>of such a package fails withPublishForbidden.
exclude: entries in ohpm-workspace.yaml remove a package from the workspace
entirely; publish: false keeps it a member (so others can still depend on it
via file:) but marks it non-publishable.
ohpm-rs keeps every downloaded package in a content-addressed store shared
by all projects on the machine — the cache config directory (default
~/.ohpm/cache), pnpm-store aligned:
<cache>/
content-v1/<alg>/<h[0:2]>/<h[2:4]>/<h[4:]> # downloaded archives, keyed by digest
extracted-v1/<alg>/<h[0:2]>/<h[2:4]>/<h[4:]> # shared extracted trees (hard-link mode only)
harball/ # publish staging
The layout matches real ohpm byte-for-byte, so ohpm-rs and the reference tool
share one cache: each side reuses the other's downloads (ohpm DEBUG: found package ... from cache file).
Configure the store (pnpm-style resolution: env > CLI > project .ohpmrc >
user ~/.ohpm/.ohpmrc > default):
ohpm-rs config set cache /path/to/store # persist (user rc)
OHPM_CACHE=/path/to/store ohpm-rs install # per-invocation
ohpm-rs install --cache /path/to/store # per-command
ohpm-rs cache path # print the effective store dirManage it (≈ pnpm store):
ohpm-rs cache path # effective store dir
ohpm-rs cache status # verify every cached archive's integrity
ohpm-rs cache add @ohos/foo@1.2.3 # pre-fetch into the store (no install)
ohpm-rs cache clean # drop content-v1 + harball (+ extracted-v1)cache add accepts registry packages only (name[@version | @tag:<tag>];
tag:latest is invalid, like the reference); repeat runs are served from the
store.
By default each project extracts its own copy of a package into oh_modules
(reference behavior). With cache_hardlink=true (via config set,
OHPM_CACHE_HARDLINK, or ~/.ohpm/.ohpmrc), the archive is extracted once
into the shared extracted-v1 layer and every project's store dir is
hard-linked from it — pnpm's zero-copy reuse: two projects installing the same
version share the same files on disk (copy fallback on cross-device links).
⚠️ Hard links share inodes: a build tool that modifies files insideoh_moduleswould corrupt the shared layer for every project using it. Keep the default off unless you understand this trade-off.
- Upload paths: packages above
use_stream_threshold_size(default 5 MB) go throughPOST {registry}stream/{name}(multipartmetadata+pkg_stream); smaller packages go throughPUT {registry}{name}with a JSON body carrying base64_attachments. Both match the reference uploaders, including the 598-locked retry (3 attempts, 30 s) and the stream→attachment fallback. oh-package.json5is parsed as JSON5; unknown fields round-trip into the published metadata.- The
.tgz(HSP) bundle path (InterfaceHar+.hsp) is implemented. - Out of scope: script hooks, conflict resolution (strict/overrides), unified
lockfiles, HSP packages, the
config encryptcrypto component, and rayon-accelerated extraction (the pipeline currently usesspawn_blocking+ a semaphore).