All docs

Build & packaging reference

Build & packaging reference, from Ophio's own documentation.

Ophio isn’t released yet. These docs describe the development version; app downloads are not available.

Product-owned code is Apache-2.0. The display name comes from client/src/brand.ts; release scripts derive their archive names from it. Cargo.toml’s workspace version is authoritative. Packaging rejects a different version in the client package, native client manifest, or Tauri configuration. After changing the display name, regenerate NOTICE with python3 scripts/package-artifacts.py notice.

Prerequisites

  • Rust/Cargo 1.92 or newer, edition 2024; current builds use 1.98.
  • Python 3.11 or newer; packaging and licence scripts use only its standard library.
  • Node.js supported by the locked Vite release and npm with lockfile v3 support.
  • Linux x86_64: C/C++ compiler, pkg-config, libclang, PipeWire development files, and the Wayland/X11 libraries required by the platform adapter. Use your distribution’s packages; the build scripts never install system dependencies.
  • Android: JDK 21, Android SDK platform/build-tools 37, NDK 28.2.13676358, the aarch64-linux-android Rust target, and the locked Gradle wrapper. The project uses Gradle 9.6.1, Android Gradle Plugin 9.3.1 and Kotlin 2.2.10.
  • Windows cross-build: the x86_64-pc-windows-gnullvm Rust target and LLVM/MinGW. Configure Cargo’s target linker and C/C++ compiler variables for that target. The current runtime-notice baseline is LLVM 23.1.2. Do not silently substitute a different static runtime without updating its inventory and notices.
  • Advisory checks (scripts/ci.sh advisories): cargo-audit 0.22.2 and network access. The stage finds cargo-audit on PATH, or else under $BUILD_TOOLS_HOME/cargo-audit/bin when BUILD_TOOLS_HOME is set. Install it into the build tools directory, not the user’s Cargo home: cargo install --locked --version 0.22.2 --root "$BUILD_TOOLS_HOME/cargo-audit" cargo-audit. The stage audits both Cargo lock files and npm audit --omit=dev in client/, denying warnings, and fails when the tool is missing or a fetch fails, even with CI_ALLOW_MISSING_TOOLS=1. Each ignored advisory, with its reason, lives in .cargo/audit.toml for the root lock file and client/src-tauri/.cargo/audit.toml for the Tauri lock file. The hosted Checks workflow runs this stage as its own advisories job, apart from the build, so a newly published advisory fails that job by name.

Use a build-specific toolchain environment. The scripts accept BUILD_ENV_FILE=/path/to/build-env.sh, or an environment sourced before invoking them. It must supply JAVA_HOME, ANDROID_HOME, NDK_HOME, target linkers and any dedicated CARGO_HOME/RUSTUP_HOME/GRADLE_USER_HOME/npm cache directories. No script modifies shell startup files or global tool configuration.

export CARGO_TARGET_DIR="$PWD/target" CARGO_BUILD_JOBS=8
cargo build --locked -p companion -p computer-mcp
cargo fmt --all
cargo clippy --workspace --all-targets -- -D warnings
env -u DISPLAY -u WAYLAND_DISPLAY -u DBUS_SESSION_BUS_ADDRESS cargo test --workspace
npm --prefix client ci --ignore-scripts --no-audit --no-fund
npm --prefix client run typecheck
npm --prefix client test
npm --prefix client run build
python3 -m unittest discover -s scripts/tests -v

The normal tests use fakes, isolated files and loopback; desktop adapter tests must run with display/session variables unset. Never enable desktop capture or run real agent inference merely to verify a build. Native Android and Windows runtime qualification are separate from compilation.

Offline preparation

Both Cargo workspaces have independent lockfiles. Populate the selected Cargo registry cache before disconnecting:

cargo fetch --locked --target x86_64-unknown-linux-gnu
cargo fetch --locked --target x86_64-pc-windows-gnullvm
cargo fetch --locked --manifest-path client/src-tauri/Cargo.toml --target aarch64-linux-android
npm --prefix client ci --ignore-scripts --no-audit --no-fund

Android additionally needs the Gradle distribution, plugins, runtime dependency POMs/JARs/AARs, SDK and NDK already cached. Resolve those with the standard Tauri Android build in your own prepared build environment before requiring offline operation. Packaging forces Cargo, npm and Gradle offline and reports missing cache entries. It never fetches speech models. Once cached, add --offline to Cargo commands and npm ci; no registry credentials are needed for these public dependencies.

Gradle checks its distribution against the wrapper’s distributionSha256Sum (Gradle’s published value from https://gradle.org/release-checksums/) and every plugin, POM and library against SHA-256 entries in client/src-tauri/gen/android/gradle/verification-metadata.xml, and scripts/tests/test_android_verification.py pins gradle-wrapper.jar to Gradle’s published wrapper checksum from the same page. After changing the Gradle version or an Android dependency, update both checksums and regenerate the metadata in client/src-tauri/gen/android/ after one Tauri Android build. Use an empty Gradle home, since a warm cache omits parent POMs, and review the diff before committing.

rm gradle/verification-metadata.xml
GRADLE_USER_HOME="$(mktemp -d)" ./gradlew --write-verification-metadata sha256 \
  -x rustBuildUniversalDebug -x rustBuildUniversalRelease \
  assembleUniversalDebug assembleUniversalRelease bundleUniversalRelease \
  :tauri-plugin-device:assembleDebugAndroidTest :tauri-plugin-device:lintDebug

Local release candidates

Run one command from a clean checkout with prerequisites and dependency caches prepared:

PlatformCommandSuccessful output
Linuxscripts/package-linux.shdist/linux/<name>-<version>-x86_64-linux.tar.gz
Windows, cross-build onlyscripts/package-windows.shdist/windows/<name>-<version>-x86_64-windows.zip
Android ARM64scripts/package-android.shunsigned APK/AAB and separate locally test-signed APK under dist/android/

All scripts generate and check licences before final distribution assembly. The archives contain both companion binaries, LICENSE, NOTICE, THIRD-PARTY-NOTICES.txt, sbom.cdx.json, the licence review report, and platform installation instructions. Linux additionally includes install.sh and verifies the extracted companion’s --version and --help using isolated application directories. It never installs or starts a service; ophio service install writes the user unit when someone runs it. Windows checks x64 PE headers; no Windows runtime claim follows from that check.

dist/MANIFEST.txt lists every distribution file, byte count and SHA-256. Archive entry order, timestamps and modes are stable for identical inputs; SOURCE_DATE_EPOCH defaults to the current commit timestamp. This does not claim bit-identical Rust/Gradle rebuilds across different toolchains or source paths. The temporary Android test certificate changes on every packaging invocation. Release builds remap checkout and toolchain-cache paths in Rust and native C/C++ diagnostics so packaged executables do not disclose the builder’s local paths.

Android builds in target/packaging/android/project/, copied from tracked checkout inputs. Tauri’s generated native files never modify the checkout. Its first build resolves the native project, then an offline Gradle report records universalReleaseRuntimeClasspath, including AndroidX, Kotlin and plugin runtime dependencies. The second build embeds verified licence assets. Generated assets are available at client/dist/licenses/ in that staged build and as APK entries assets/licenses/{LICENSE,NOTICE,THIRD-PARTY-NOTICES.txt,sbom.cdx.json}. Settings → Versions → Open-source notices shows the bundled NOTICE and THIRD-PARTY-NOTICES.txt from /licenses/ through Tauri’s embedded frontend; a development build without them says so. The script verifies all four packaged native asset bytes.

The locally test-signed APK contains the release build, signed with a fresh throwaway Android Debug certificate, not a production signing identity. The keystore and password exist only in a temporary build-output directory and are deleted after signing. apksigner verify --verbose --print-certs and aapt2 dump badging validate the test copy; identity, version and ARM64 ABI are checked against the source configuration. No emulator or physical device is started or installed to by packaging.

Real APK release signing is deliberately left out. Once a release key is chosen, the exact separate step is:

zipalign -f -P 16 4 unsigned.apk release-aligned.apk
apksigner sign --ks /path/to/release.keystore --ks-key-alias RELEASE_ALIAS \
  --out release.apk release-aligned.apk
apksigner verify --verbose --print-certs release.apk

Allow the signing tool to prompt for passwords; never put them in a script or argument. AAB signing, if selected for distribution, uses the upload key: jarsigner -keystore /path/to/upload.keystore -signedjar release.aab unsigned.aab UPLOAD_ALIAS, then jarsigner -verify -verbose release.aab. No real signing, store submission, publishing, installation or upload occurs in these scripts.

Licence inventory and release checks

python3 scripts/licenses.py --check
scripts/source-candidate.sh --json target/packaging/source-candidate.json
python3 scripts/earlier_names.py

The offline generator reads locked Cargo metadata and uses Cargo’s target-specific normal/build dependency tree for the two host binaries or native phone shell. It excludes development edges and test-only crates. Build inputs are deliberately included even when the linker discards them. For the web client it conservatively includes the npm production dependency closure, including peers; it does not claim an exact Rollup/Rolldown tree-shaken module inventory. Bundled font files are identified by their hashes when the Android script supplies the built web directory. Standalone checks without it inventory all production font files. Icons, native runtime libraries, embedded SQLite/IJG material and pinned speech model downloads have separate entries. Models are labelled external runtime components and are never shipped as weight files.

Cargo hashes identify registry archives; npm integrity hashes identify package archives. Maven artifacts and font files have computed SHA-256 values. Model hashes are read from the speech catalog. Artifact hashes are in the distribution manifest. Missing Gradle reports, licence texts or unrecognized licence terms fail the check; an incomplete SBOM is explicitly marked incomplete and is never silently promoted into an archive. MPL/LGPL obligations appear as review items. The generator retains all supplied licence, copyright and NOTICE files, even when the selected licence expression permits a simpler alternative.

packaging/license-policy.toml holds policy and any exact, reasoned exceptions. packaging/license-evidence.toml records hash-pinned upstream texts omitted from published crate archives, fetched at their Cargo VCS revisions. These are supplemental evidence, not policy waivers. Static runtime inventory changes need review whenever a compiler, codec feature or platform target changes.

The source checker examines git archive HEAD, not uncommitted edits or the repository history. It reports every forbidden path/content location and missing owned-source SPDX header without echoing potentially private line contents. That includes home directories on Linux, macOS and Windows, files named after work items (such as U32.json) and notes on how the product was built. When the Git directory holds an untracked info/private-terms file (one term per line, # for comments), each term found in a path or line is reported by rule and location, never by the term itself, and paths are shown with the term masked. Only marked generated bindings have a documented header exception. Fix and commit all findings, then rerun it. A passing scan is a bounded mechanical check, not a substitute for reviewing a proposed public export for confidential text.

scripts/earlier_names.py fails when a tracked file uses the identity the product had before it was named: its Android namespace, link schemes, variables, Library file prefix, directories, commands or display name. Its ALLOWED list names the files that carry an earlier install across, each with its reason, and fails when one of them no longer needs the exception. The CI source stage runs it.

Build a release from a clean tree after these checks and a review of its content.

Linux release executables use glibc 2.39 as their maximum ABI requirement (Ubuntu 24.04). A build on a newer host is not a portable release: packaging checks the version requirements of both ophio and computer-mcp with readelf and rejects anything above that ceiling.

The disposable Ubuntu 24.04 lab guest can build offline from committed source and vendored dependencies using tools/lab/guest/release.sh. It reuses the installed Rust 1.98 toolchain and native libraries, writes only to the lab user’s build directories, and installs no packages. This baseline was chosen because the existing guest provides a reproducible older userspace without another image or hosted build service. Extracted release archives must also be exercised there; a successful build alone is not runtime qualification.

Imported from BUILDING.md. Original product documentation is Apache-2.0; licence notices.