diff --git a/.github/workflows/nix-check.yml b/.github/workflows/nix-check.yml
new file mode 100644
index 000000000..88e8d8944
--- /dev/null
+++ b/.github/workflows/nix-check.yml
@@ -0,0 +1,103 @@
+name: Nix flake checks
+
+on:
+ pull_request:
+ paths:
+ - "Cargo.toml"
+ - "Cargo.lock"
+ - "flake.nix"
+ - "flake.lock"
+ - "nix/release-manifest.json"
+ - "nix/integration/**"
+ - "crates/op-host-desktop/assets/icon.png"
+ - ".github/workflows/nix-check.yml"
+ push:
+ branches: ["**"]
+ paths:
+ - "Cargo.toml"
+ - "Cargo.lock"
+ - "flake.nix"
+ - "flake.lock"
+ - "nix/release-manifest.json"
+ - "nix/integration/**"
+ - "crates/op-host-desktop/assets/icon.png"
+ - ".github/workflows/nix-check.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ flake:
+ name: Build and smoke-test prebuilt outputs
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install Nix
+ uses: DeterminateSystems/nix-installer-action@main
+
+ - name: Enable Nix cache
+ uses: DeterminateSystems/magic-nix-cache-action@main
+
+ - name: Evaluate flake checks
+ run: nix flake check --no-build --show-trace
+
+ - name: Build prebuilt packages
+ run: nix build .#prebuilt .#prebuilt-cli --no-link
+
+ - name: Build representative source outputs
+ run: |
+ nix build \
+ .#op-cli \
+ .#web-server \
+ .#web-bundle \
+ .#web-sdk-wasm \
+ .#web-sdk-packages \
+ .#appimage \
+ --no-link
+
+ - name: Verify generated integration sidecar
+ run: nix build .#checks.x86_64-linux.integration-sidecar --no-link
+
+ - name: Install headless display server
+ run: |
+ sudo apt-get update
+ sudo apt-get install --yes \
+ libegl1 libgles2 libgl1-mesa-dri libgbm1 mesa-utils xvfb
+
+ - name: Smoke-test prebuilt CLI
+ run: |
+ output="$(nix run .#prebuilt-cli -- --version)"
+ printf '%s\n' "$output"
+ printf '%s\n' "$output" | grep -Eq '^\{"version":"[0-9]+\.[0-9]+\.[0-9]+"\}$'
+
+ - name: Smoke-test prebuilt desktop
+ shell: bash
+ run: |
+ set +e
+ LIBGL_ALWAYS_SOFTWARE=1 \
+ MESA_LOADER_DRIVER_OVERRIDE=llvmpipe \
+ GALLIUM_DRIVER=llvmpipe \
+ timeout 20s xvfb-run -a -s "-screen 0 1280x720x24" nix run .#prebuilt \
+ >/tmp/openpencil-desktop.log 2>&1
+ status=$?
+ set -e
+
+ # A GUI process is expected to remain alive until timeout. Any other
+ # non-zero status means startup failed before the process was usable.
+ if [[ "$status" -ne 0 && "$status" -ne 124 ]]; then
+ cat /tmp/openpencil-desktop.log
+ exit "$status"
+ fi
+ if grep -Eq 'error while loading|cannot open shared object' \
+ /tmp/openpencil-desktop.log; then
+ cat /tmp/openpencil-desktop.log
+ exit 1
+ fi
+
+ - name: Run the clean NixOS runtime test
+ run: nix build .#checks.x86_64-linux.prebuilt-runtime --no-link
diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml
index 8d14b65ae..01ca133e9 100644
--- a/.github/workflows/rust-release.yml
+++ b/.github/workflows/rust-release.yml
@@ -870,6 +870,14 @@ jobs:
echo "::error::missing op-cli artifacts"
exit 1
fi
+ for file in \
+ release-files/openpencil-desktop-linux-x86_64.tar.gz \
+ release-files/op-cli-linux-x86_64.tar.gz; do
+ if [ ! -f "$file" ]; then
+ echo "::error::missing Linux x86_64 Nix manifest asset: $file"
+ exit 1
+ fi
+ done
if [ "${#sdk[@]}" -ne 3 ]; then
echo "::error::expected 3 SDK tarballs, found ${#sdk[@]}"
exit 1
@@ -959,6 +967,34 @@ jobs:
name: ${{ steps.body.outputs.title }}
body_path: release-body.md
files: release-files/*
+ - name: Update latest published Nix release manifest
+ env:
+ GH_TOKEN: ${{ github.token }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ version="$OP_VERSION"
+ sri_hash() {
+ printf 'sha256-%s' "$(openssl dgst -sha256 -binary "release-files/$1" | base64 -w0)"
+ }
+ manifest="$(jq -n \
+ --arg version "$version" \
+ --arg desktopHash "$(sri_hash openpencil-desktop-linux-x86_64.tar.gz)" \
+ --arg cliHash "$(sri_hash op-cli-linux-x86_64.tar.gz)" \
+ '{version: $version, desktopHash: $desktopHash, cliHash: $cliHash}')"
+
+ git fetch origin main
+ git switch --create update-release-manifest origin/main
+ printf '%s\n' "$manifest" > nix/release-manifest.json
+ git config user.name github-actions[bot]
+ git config user.email 41898282+github-actions[bot]@users.noreply.github.com
+ git add nix/release-manifest.json
+ if git diff --cached --quiet; then
+ echo "release manifest already current"
+ exit 0
+ fi
+ git commit -m "chore(release): update Nix release manifest for v${version}"
+ git push origin HEAD:main
package-managers:
name: Update Homebrew tap and Scoop bucket
diff --git a/README.md b/README.md
index b594fccdb..52ac76037 100644
--- a/README.md
+++ b/README.md
@@ -147,6 +147,28 @@ scoop install openpencil
**Linux / Windows direct download:** [GitHub Releases](https://github.com/ZSeven-W/openpencil/releases) — `.exe` (Windows), `.AppImage` / `.deb` (Linux)
+**Nix (Linux x86_64):**
+
+```bash
+nix develop
+nix run . # launch the desktop app
+nix build .#openpencil # native web host + CanvasKit web bundle
+nix build .#op-cli # the `op` CLI
+nix build .#prebuilt # use the matching upstream desktop archive
+nix build .#prebuilt-cli # use the matching upstream CLI archive
+nix build .#web-server # native GL-free web server + web bundle
+nix build .#runtime-prebuilt # prebuilt desktop + `op` CLI runtime
+nix build .#web-sdk-packages # npm tarballs for the web SDKs
+nix build .#appimage # portable desktop AppImage
+```
+
+The flake uses the pinned Rust toolchain from `rust-toolchain.toml` and is
+currently published for `x86_64-linux`. A Debian package is not produced by
+the flake yet; use the upstream release artifacts when a `.deb` is required.
+The `prebuilt` outputs require the upstream GitHub release for the workspace
+version to publish matching Linux archives; otherwise use the source-built
+outputs above.
+
**CLI (`op`):**
```bash
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 000000000..e9ed0de2b
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,1117 @@
+{
+ "nodes": {
+ "agent": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1783431758,
+ "narHash": "sha256-kh5XrS8ZZOhwk52nIvXGdyqn8F5nSlL2qGbUel+eJhw=",
+ "owner": "ZSeven-W",
+ "repo": "agent-rs",
+ "rev": "5c0f9506e27be5b4f29cd2c1093e2858ad0fa20c",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ZSeven-W",
+ "repo": "agent-rs",
+ "rev": "5c0f9506e27be5b4f29cd2c1093e2858ad0fa20c",
+ "type": "github"
+ }
+ },
+ "bun-overlay": {
+ "inputs": {
+ "flake-compat": "flake-compat",
+ "flake-utils": "flake-utils",
+ "nixpkgs": [
+ "js-harbor",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1782500366,
+ "narHash": "sha256-o1eg50x2p7YYgX2UZ+qJ8Z6nnYQ616LDg62TkCn5Llc=",
+ "owner": "alleneubank",
+ "repo": "bun-overlay",
+ "rev": "288d81fd8c4960d910a98263b73381d2b771b4d9",
+ "type": "github"
+ },
+ "original": {
+ "owner": "alleneubank",
+ "repo": "bun-overlay",
+ "type": "github"
+ }
+ },
+ "casement": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1780160489,
+ "narHash": "sha256-kRfgBHG8XLIbfLUV+6mFF1eWfrlp4NlDCa3Ql99lkc4=",
+ "owner": "ZSeven-W",
+ "repo": "casement",
+ "rev": "451173eb3353a4166d2d0f241f2e0606051064bd",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ZSeven-W",
+ "repo": "casement",
+ "rev": "451173eb3353a4166d2d0f241f2e0606051064bd",
+ "type": "github"
+ }
+ },
+ "crane": {
+ "locked": {
+ "lastModified": 1784564969,
+ "narHash": "sha256-TkTWNhJV/8Wk3czlbz4bwHZkFCaCHPsOy+oJe4PUiXg=",
+ "owner": "ipetkov",
+ "repo": "crane",
+ "rev": "7930f6c291de6f83c257839d434592aa085f290a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ipetkov",
+ "repo": "crane",
+ "type": "github"
+ }
+ },
+ "crane_2": {
+ "locked": {
+ "lastModified": 1781825982,
+ "narHash": "sha256-SlXKwIRIhrOSAcTjCB3ftPLzJWZStQIPS7J1FlZPnKk=",
+ "owner": "ipetkov",
+ "repo": "crane",
+ "rev": "469fd08d0bcf6926321fa973c6777fbc87785dd7",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ipetkov",
+ "repo": "crane",
+ "type": "github"
+ }
+ },
+ "crane_3": {
+ "locked": {
+ "lastModified": 1781825982,
+ "narHash": "sha256-SlXKwIRIhrOSAcTjCB3ftPLzJWZStQIPS7J1FlZPnKk=",
+ "owner": "ipetkov",
+ "repo": "crane",
+ "rev": "469fd08d0bcf6926321fa973c6777fbc87785dd7",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ipetkov",
+ "repo": "crane",
+ "type": "github"
+ }
+ },
+ "flake-compat": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1733328505,
+ "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
+ "type": "github"
+ },
+ "original": {
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "flake-compat_2": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1733328505,
+ "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
+ "type": "github"
+ },
+ "original": {
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "flake-parts": {
+ "inputs": {
+ "nixpkgs-lib": "nixpkgs-lib"
+ },
+ "locked": {
+ "lastModified": 1782949081,
+ "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "type": "github"
+ }
+ },
+ "flake-parts_2": {
+ "inputs": {
+ "nixpkgs-lib": "nixpkgs-lib_2"
+ },
+ "locked": {
+ "lastModified": 1782949081,
+ "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "type": "github"
+ }
+ },
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_10": {
+ "inputs": {
+ "systems": "systems_10"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_2": {
+ "inputs": {
+ "systems": "systems_2"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_3": {
+ "inputs": {
+ "systems": "systems_3"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_4": {
+ "inputs": {
+ "systems": "systems_4"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_5": {
+ "inputs": {
+ "systems": "systems_5"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_6": {
+ "inputs": {
+ "systems": "systems_6"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_7": {
+ "inputs": {
+ "systems": "systems_7"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_8": {
+ "inputs": {
+ "systems": "systems_8"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_9": {
+ "inputs": {
+ "systems": "systems_9"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "jian": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1784811987,
+ "narHash": "sha256-9SPcztfg1WvPSKH7SHu3rYZa5X9ifpm3LJzMRAFiTJU=",
+ "owner": "ZSeven-W",
+ "repo": "jian",
+ "rev": "df2376a018acbd8ec8d9fac58b05dde14a405aca",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ZSeven-W",
+ "repo": "jian",
+ "rev": "df2376a018acbd8ec8d9fac58b05dde14a405aca",
+ "type": "github"
+ }
+ },
+ "js-harbor": {
+ "inputs": {
+ "bun-overlay": "bun-overlay",
+ "flake-parts": [
+ "flake-parts"
+ ],
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1783519395,
+ "narHash": "sha256-enwThdgyM0CnQn/YNWL40sTN80QlZgxl9vSN3LYF7Mg=",
+ "ref": "trunk",
+ "rev": "f8f435b29934fa19af68d0485869db2df6f7eea5",
+ "revCount": 3,
+ "type": "git",
+ "url": "https://codeberg.org/caniko/js-harbor.git"
+ },
+ "original": {
+ "ref": "trunk",
+ "type": "git",
+ "url": "https://codeberg.org/caniko/js-harbor.git"
+ }
+ },
+ "meta-harbor": {
+ "inputs": {
+ "flake-utils": "flake-utils_4",
+ "nixpkgs": [
+ "nix-pklx",
+ "rs-harbor",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1783280759,
+ "narHash": "sha256-kXNLMVs0bF1DzeK8NPG54mY5wwvdng8XDep3C/uqHhE=",
+ "ref": "trunk",
+ "rev": "d189903b657f4d0dc4f47003005f1b9260c77e89",
+ "revCount": 2,
+ "type": "git",
+ "url": "https://codeberg.org/caniko/meta-harbor.git"
+ },
+ "original": {
+ "ref": "trunk",
+ "type": "git",
+ "url": "https://codeberg.org/caniko/meta-harbor.git"
+ }
+ },
+ "meta-harbor_2": {
+ "inputs": {
+ "flake-utils": [
+ "py-harbor",
+ "flake-utils"
+ ],
+ "nixpkgs": [
+ "py-harbor",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1783280759,
+ "narHash": "sha256-kXNLMVs0bF1DzeK8NPG54mY5wwvdng8XDep3C/uqHhE=",
+ "ref": "trunk",
+ "rev": "d189903b657f4d0dc4f47003005f1b9260c77e89",
+ "revCount": 2,
+ "type": "git",
+ "url": "https://codeberg.org/caniko/meta-harbor.git"
+ },
+ "original": {
+ "ref": "trunk",
+ "type": "git",
+ "url": "https://codeberg.org/caniko/meta-harbor.git"
+ }
+ },
+ "meta-harbor_3": {
+ "inputs": {
+ "flake-utils": "flake-utils_8",
+ "nixpkgs": [
+ "rs-harbor",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1783280759,
+ "narHash": "sha256-kXNLMVs0bF1DzeK8NPG54mY5wwvdng8XDep3C/uqHhE=",
+ "ref": "trunk",
+ "rev": "d189903b657f4d0dc4f47003005f1b9260c77e89",
+ "revCount": 2,
+ "type": "git",
+ "url": "https://codeberg.org/caniko/meta-harbor.git"
+ },
+ "original": {
+ "ref": "trunk",
+ "type": "git",
+ "url": "https://codeberg.org/caniko/meta-harbor.git"
+ }
+ },
+ "nix-appimage": {
+ "inputs": {
+ "flake-compat": "flake-compat_2",
+ "flake-utils": "flake-utils_2",
+ "nixpkgs": "nixpkgs"
+ },
+ "locked": {
+ "lastModified": 1757920913,
+ "narHash": "sha256-jd0QwCVz4O1sHHkeaZILD/7D6oyalceEJ4EFnWCgm0k=",
+ "owner": "ralismark",
+ "repo": "nix-appimage",
+ "rev": "7946addbc0d97e358a6d7aefe5e82310f0fe6b18",
+ "type": "github"
+ },
+ "original": {
+ "owner": "ralismark",
+ "repo": "nix-appimage",
+ "type": "github"
+ }
+ },
+ "nix-opencode-lsp": {
+ "inputs": {
+ "flake-utils": "flake-utils_5",
+ "nixpkgs": [
+ "nix-pklx",
+ "rs-harbor",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1783251082,
+ "narHash": "sha256-XfHlECWS/u6FWjYAMOsGk6pcGx+z3HA8WWGWR7VvTSI=",
+ "ref": "refs/heads/trunk",
+ "rev": "a45718d96cdde0101ef8c20942fada0b188745f0",
+ "revCount": 1,
+ "type": "git",
+ "url": "ssh://git@codeberg.org/caniko/nix-opencode-lsp.git"
+ },
+ "original": {
+ "type": "git",
+ "url": "ssh://git@codeberg.org/caniko/nix-opencode-lsp.git"
+ }
+ },
+ "nix-opencode-lsp_2": {
+ "inputs": {
+ "flake-utils": [
+ "py-harbor",
+ "flake-utils"
+ ],
+ "nixpkgs": [
+ "py-harbor",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1783251302,
+ "narHash": "sha256-F0ij0ZsjrPpqPJc02zv15ZkEbU97XBE/SWrZ9LN8JZE=",
+ "ref": "refs/heads/trunk",
+ "rev": "0f9b797ed177cdc78d34d085fff9ff7c45a51b2f",
+ "revCount": 2,
+ "type": "git",
+ "url": "https://codeberg.org/caniko/nix-opencode-lsp.git"
+ },
+ "original": {
+ "type": "git",
+ "url": "https://codeberg.org/caniko/nix-opencode-lsp.git"
+ }
+ },
+ "nix-opencode-lsp_3": {
+ "inputs": {
+ "flake-utils": "flake-utils_9",
+ "nixpkgs": [
+ "rs-harbor",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1783251082,
+ "narHash": "sha256-XfHlECWS/u6FWjYAMOsGk6pcGx+z3HA8WWGWR7VvTSI=",
+ "ref": "refs/heads/trunk",
+ "rev": "a45718d96cdde0101ef8c20942fada0b188745f0",
+ "revCount": 1,
+ "type": "git",
+ "url": "https://codeberg.org/caniko/nix-opencode-lsp.git"
+ },
+ "original": {
+ "type": "git",
+ "url": "https://codeberg.org/caniko/nix-opencode-lsp.git"
+ }
+ },
+ "nix-pklx": {
+ "inputs": {
+ "crane": "crane_2",
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "plinth": "plinth",
+ "rs-harbor": "rs-harbor",
+ "rust-overlay": "rust-overlay_2"
+ },
+ "locked": {
+ "lastModified": 1784352314,
+ "narHash": "sha256-0jwdNIJ0fINMCT8Cz3M+eyGYfbMDJtJGBIHySq6bQwc=",
+ "ref": "refs/heads/trunk",
+ "rev": "6fbee47944dc1032e3fe48a16bdc69c57406dc6a",
+ "revCount": 26,
+ "type": "git",
+ "url": "https://codeberg.org/caniko/nix-pklx.git"
+ },
+ "original": {
+ "type": "git",
+ "url": "https://codeberg.org/caniko/nix-pklx.git"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1745997950,
+ "narHash": "sha256-FklMkU+bPPx0L3i6pdrw65js05VO9He3DTeKguX9GQY=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "cf3ffa5d140899101f1deb3f4d16b1a1aa2de849",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs-lib": {
+ "locked": {
+ "lastModified": 1782614948,
+ "narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=",
+ "owner": "nix-community",
+ "repo": "nixpkgs.lib",
+ "rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "repo": "nixpkgs.lib",
+ "type": "github"
+ }
+ },
+ "nixpkgs-lib_2": {
+ "locked": {
+ "lastModified": 1782614948,
+ "narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=",
+ "owner": "nix-community",
+ "repo": "nixpkgs.lib",
+ "rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "repo": "nixpkgs.lib",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1784497964,
+ "narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "osxcross": {
+ "inputs": {
+ "flake-utils": "flake-utils_6",
+ "nixpkgs": [
+ "nix-pklx",
+ "rs-harbor",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1780425807,
+ "narHash": "sha256-vPrpqec/uHpOu0u4P+XdL70MWaHyn4zh8IfpCmp1yIs=",
+ "owner": "caniko",
+ "repo": "osxcross",
+ "rev": "246fc035534167fadba187a602bc4a40990974dc",
+ "type": "github"
+ },
+ "original": {
+ "owner": "caniko",
+ "ref": "flake",
+ "repo": "osxcross",
+ "type": "github"
+ }
+ },
+ "osxcross_2": {
+ "inputs": {
+ "flake-utils": "flake-utils_10",
+ "nixpkgs": [
+ "rs-harbor",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1780425807,
+ "narHash": "sha256-vPrpqec/uHpOu0u4P+XdL70MWaHyn4zh8IfpCmp1yIs=",
+ "owner": "caniko",
+ "repo": "osxcross",
+ "rev": "246fc035534167fadba187a602bc4a40990974dc",
+ "type": "github"
+ },
+ "original": {
+ "owner": "caniko",
+ "ref": "flake",
+ "repo": "osxcross",
+ "type": "github"
+ }
+ },
+ "plinth": {
+ "inputs": {
+ "crane": "crane_3",
+ "flake-utils": "flake-utils_3",
+ "nixpkgs": [
+ "nix-pklx",
+ "nixpkgs"
+ ],
+ "rust-overlay": "rust-overlay"
+ },
+ "locked": {
+ "lastModified": 1782557969,
+ "narHash": "sha256-Hmp3x+SbyvWxATAqGNpJTwk94T5Kxf4z8xuqeX/Tib0=",
+ "ref": "refs/heads/trunk",
+ "rev": "316b96c0b6628ddb42b4bce80010cc6978508dce",
+ "revCount": 80,
+ "type": "git",
+ "url": "https://codeberg.org/caniko/plinth.git"
+ },
+ "original": {
+ "type": "git",
+ "url": "https://codeberg.org/caniko/plinth.git"
+ }
+ },
+ "py-harbor": {
+ "inputs": {
+ "flake-utils": "flake-utils_7",
+ "meta-harbor": "meta-harbor_2",
+ "nix-opencode-lsp": "nix-opencode-lsp_2",
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "pyproject-build-systems": "pyproject-build-systems",
+ "pyproject-nix": "pyproject-nix",
+ "uv2nix": "uv2nix"
+ },
+ "locked": {
+ "lastModified": 1784653399,
+ "narHash": "sha256-NlaT1Y2tb0gGFX6K4AEMee2/k0C+xAKvknQzwwB+Tzo=",
+ "ref": "trunk",
+ "rev": "5212a81218d9c59448b361f9b08091d4ac9512db",
+ "revCount": 9,
+ "type": "git",
+ "url": "https://codeberg.org/caniko/py-harbor.git"
+ },
+ "original": {
+ "ref": "trunk",
+ "type": "git",
+ "url": "https://codeberg.org/caniko/py-harbor.git"
+ }
+ },
+ "pyproject-build-systems": {
+ "inputs": {
+ "nixpkgs": [
+ "py-harbor",
+ "nixpkgs"
+ ],
+ "pyproject-nix": [
+ "py-harbor",
+ "pyproject-nix"
+ ],
+ "uv2nix": [
+ "py-harbor",
+ "uv2nix"
+ ]
+ },
+ "locked": {
+ "lastModified": 1782093830,
+ "narHash": "sha256-6gmEVe69+KlRkZD4PEEV5xAlB9CB0Y9TiuEgQjDrKTQ=",
+ "owner": "pyproject-nix",
+ "repo": "build-system-pkgs",
+ "rev": "430680a19bc85a3bda55f12e4cc1a1aadcf2e478",
+ "type": "github"
+ },
+ "original": {
+ "owner": "pyproject-nix",
+ "repo": "build-system-pkgs",
+ "type": "github"
+ }
+ },
+ "pyproject-nix": {
+ "inputs": {
+ "nixpkgs": [
+ "py-harbor",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1782905613,
+ "narHash": "sha256-SvXJcAemihifkTn4BGvyE5K1FJX9bl4U8DQ5pqKvD0s=",
+ "owner": "pyproject-nix",
+ "repo": "pyproject.nix",
+ "rev": "7af23cfe91064865ecf2e835da28b45b3c6f49fd",
+ "type": "github"
+ },
+ "original": {
+ "owner": "pyproject-nix",
+ "repo": "pyproject.nix",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "agent": "agent",
+ "casement": "casement",
+ "crane": "crane",
+ "flake-parts": "flake-parts",
+ "jian": "jian",
+ "js-harbor": "js-harbor",
+ "nix-appimage": "nix-appimage",
+ "nix-pklx": "nix-pklx",
+ "nixpkgs": "nixpkgs_2",
+ "py-harbor": "py-harbor",
+ "rs-harbor": "rs-harbor_2",
+ "rust-overlay": "rust-overlay_3"
+ }
+ },
+ "rs-harbor": {
+ "inputs": {
+ "crane": [
+ "nix-pklx",
+ "crane"
+ ],
+ "flake-parts": "flake-parts_2",
+ "meta-harbor": "meta-harbor",
+ "nix-opencode-lsp": "nix-opencode-lsp",
+ "nixpkgs": [
+ "nix-pklx",
+ "nixpkgs"
+ ],
+ "osxcross": "osxcross",
+ "rust-overlay": [
+ "nix-pklx",
+ "rust-overlay"
+ ]
+ },
+ "locked": {
+ "lastModified": 1784323542,
+ "narHash": "sha256-M1Ap9kNSk4uJ18v2xrD3KVwpse1tYL0ZyUxF5aqxhvs=",
+ "ref": "trunk",
+ "rev": "9bfa8bdb0ecb22d7bc11448665f7fbaebae7a759",
+ "revCount": 134,
+ "type": "git",
+ "url": "https://codeberg.org/caniko/rs-harbor.git"
+ },
+ "original": {
+ "ref": "trunk",
+ "rev": "9bfa8bdb0ecb22d7bc11448665f7fbaebae7a759",
+ "type": "git",
+ "url": "https://codeberg.org/caniko/rs-harbor.git"
+ }
+ },
+ "rs-harbor_2": {
+ "inputs": {
+ "crane": [
+ "crane"
+ ],
+ "flake-parts": [
+ "flake-parts"
+ ],
+ "meta-harbor": "meta-harbor_3",
+ "nix-opencode-lsp": "nix-opencode-lsp_3",
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "osxcross": "osxcross_2",
+ "rust-overlay": [
+ "rust-overlay"
+ ]
+ },
+ "locked": {
+ "lastModified": 1784653386,
+ "narHash": "sha256-OQneEhYllTvdOVO0qgUxB947MFwHWRxo5FMpW0+t2bo=",
+ "ref": "trunk",
+ "rev": "827e34392640f574e8530f0438abc78d99e823e0",
+ "revCount": 147,
+ "type": "git",
+ "url": "https://codeberg.org/caniko/rs-harbor.git"
+ },
+ "original": {
+ "ref": "trunk",
+ "type": "git",
+ "url": "https://codeberg.org/caniko/rs-harbor.git"
+ }
+ },
+ "rust-overlay": {
+ "inputs": {
+ "nixpkgs": [
+ "nix-pklx",
+ "plinth",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1782184651,
+ "narHash": "sha256-4XHdXp3MRa++XiGkltJChCtdbCmSlNTwQRfWQOhqbRw=",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "rev": "f59bc28dd0b89e9c0240d1b80195559fc79c2471",
+ "type": "github"
+ },
+ "original": {
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "type": "github"
+ }
+ },
+ "rust-overlay_2": {
+ "inputs": {
+ "nixpkgs": [
+ "nix-pklx",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1782616745,
+ "narHash": "sha256-NN5B1cKBXF6h1Ec681gMGZ2o/99d7vKXAAfFNEvyOKA=",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "rev": "b916d014dc57eb555f84e51172a82f7f6fb560d7",
+ "type": "github"
+ },
+ "original": {
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "type": "github"
+ }
+ },
+ "rust-overlay_3": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1784611586,
+ "narHash": "sha256-OfqgY+0hp/zseZB7uyH0U8kIDPS4scZZCyAurEplvG0=",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "rev": "14f58845249f3552a89b07772626b8d3c632fa86",
+ "type": "github"
+ },
+ "original": {
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "type": "github"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "systems_10": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "systems_2": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "systems_3": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "systems_4": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "systems_5": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "systems_6": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "systems_7": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "systems_8": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "systems_9": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "uv2nix": {
+ "inputs": {
+ "nixpkgs": [
+ "py-harbor",
+ "nixpkgs"
+ ],
+ "pyproject-nix": [
+ "py-harbor",
+ "pyproject-nix"
+ ]
+ },
+ "locked": {
+ "lastModified": 1782810559,
+ "narHash": "sha256-loy132LKn8QfiPbFJhdTjm+yZG3zklyOQhVabLyx2l4=",
+ "owner": "pyproject-nix",
+ "repo": "uv2nix",
+ "rev": "2f698e4b5a3c6004edaf051543542f18a36afe77",
+ "type": "github"
+ },
+ "original": {
+ "owner": "pyproject-nix",
+ "repo": "uv2nix",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 000000000..7bb9a0642
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,649 @@
+{
+ description = "OpenPencil native editor, web host, and web SDK flake";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ flake-parts.url = "github:hercules-ci/flake-parts";
+ crane.url = "github:ipetkov/crane";
+ rust-overlay = {
+ url = "github:oxalica/rust-overlay";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+
+ rs-harbor = {
+ url = "git+https://codeberg.org/caniko/rs-harbor.git?ref=trunk";
+ inputs.nixpkgs.follows = "nixpkgs";
+ inputs.flake-parts.follows = "flake-parts";
+ inputs.crane.follows = "crane";
+ inputs.rust-overlay.follows = "rust-overlay";
+ };
+ py-harbor = {
+ url = "git+https://codeberg.org/caniko/py-harbor.git?ref=trunk";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ js-harbor = {
+ url = "git+https://codeberg.org/caniko/js-harbor.git?ref=trunk";
+ inputs.nixpkgs.follows = "nixpkgs";
+ inputs.flake-parts.follows = "flake-parts";
+ };
+
+ nix-appimage.url = "github:ralismark/nix-appimage";
+
+ nix-pklx = {
+ url = "git+https://codeberg.org/caniko/nix-pklx.git";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+
+ agent = {
+ url = "github:ZSeven-W/agent-rs/5c0f9506e27be5b4f29cd2c1093e2858ad0fa20c";
+ flake = false;
+ };
+ casement = {
+ url = "github:ZSeven-W/casement/451173eb3353a4166d2d0f241f2e0606051064bd";
+ flake = false;
+ };
+ jian = {
+ url = "github:ZSeven-W/jian/df2376a018acbd8ec8d9fac58b05dde14a405aca";
+ flake = false;
+ };
+ };
+
+ outputs = inputs @ {
+ self,
+ flake-parts,
+ nixpkgs,
+ crane,
+ rust-overlay,
+ rs-harbor,
+ py-harbor,
+ js-harbor,
+ nix-appimage,
+ nix-pklx,
+ agent,
+ casement,
+ jian,
+ ...
+ }:
+ flake-parts.lib.mkFlake {inherit inputs;} {
+ systems = ["x86_64-linux"];
+
+ flake = {
+ lib.integrationManifest = import ./nix/integration/openpencil.nix;
+ };
+
+ perSystem = {system, ...}: let
+ lib = pkgs.lib;
+ pkgs = import nixpkgs {
+ inherit system;
+ overlays = [(import rust-overlay)];
+ };
+ version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).workspace.package.version;
+ releaseManifest = builtins.fromJSON (builtins.readFile ./nix/release-manifest.json);
+ releaseVersion = releaseManifest.version;
+
+ toolchain = rs-harbor.lib.mkToolchain {
+ inherit pkgs;
+ toolchainFile = ./rust-toolchain.toml;
+ cache.enable = false;
+ };
+ craneLib = toolchain.craneLib;
+ rustNativeInputs = rs-harbor.lib.mkRustNativeBuildInputs {
+ inherit pkgs;
+ extra = [pkgs.pkg-config pkgs.cmake pkgs.ninja];
+ };
+ nativeLibraries = with pkgs; [
+ fontconfig
+ freetype
+ libGL
+ libdrm
+ libglvnd
+ libxkbcommon
+ mesa
+ wayland
+ libx11
+ libxcursor
+ libxext
+ libxfixes
+ libxi
+ libxrandr
+ libxrender
+ libxcb
+ libxcb-util
+ libxcb-image
+ libxcb-keysyms
+ libxcb-render-util
+ libxcb-wm
+ ];
+ runtimeLibraries = nativeLibraries;
+ # Release archives are Ubuntu-built ELF binaries. Patch their
+ # interpreter and shared-library references into the Nix store before
+ # wrapping them, otherwise they fail before LD_LIBRARY_PATH is read.
+ prebuiltDesktopRuntimeLibraries =
+ runtimeLibraries
+ ++ [
+ pkgs.zlib
+ pkgs.stdenv.cc.cc.lib
+ ];
+ prebuiltCliRuntimeLibraries = [pkgs.stdenv.cc.cc.lib];
+
+ source = pkgs.runCommand "openpencil-source-${version}" {} ''
+ mkdir -p $out/vendor/agent $out/vendor/casement $out/vendor/jian
+ cp -R --no-preserve=ownership ${./.}/. $out/
+ rm -rf $out/vendor/agent $out/vendor/casement $out/vendor/jian
+ cp -R --no-preserve=ownership ${agent}/. $out/vendor/agent
+ cp -R --no-preserve=ownership ${casement}/. $out/vendor/casement
+ cp -R --no-preserve=ownership ${jian}/. $out/vendor/jian
+ '';
+
+ skiaBinaries = pkgs.fetchurl {
+ url = "https://github.com/rust-skia/skia-binaries/releases/download/0.97.2/skia-binaries-da8fc6731fc439bc3b6a-x86_64-unknown-linux-gnu-gl-jpegd-jpege-pdf-textlayout.tar.gz";
+ hash = "sha256-7nf70Bg+hU4pcnZwXk6GhYN8bH0DBEcslxRfzY9/LPw=";
+ };
+ skiaRepositoryHash = "da8fc6731fc439bc3b6aa90d63506a808f806b26";
+ skiaGit = pkgs.writeShellScriptBin "git" ''
+ if [ "$1" = rev-parse ] && [ "$2" = --short=20 ] && [ "$3" = HEAD ]; then
+ printf '%s\n' ${skiaRepositoryHash}
+ exit 0
+ fi
+ exec ${pkgs.git}/bin/git "$@"
+ '';
+ nativeEnv = {
+ SKIA_BINARIES_URL = "file://${skiaBinaries}";
+ FORCE_SKIA_BINARIES_DOWNLOAD = "1";
+ };
+ commonArgs = {
+ inherit version;
+ src = ./.;
+ cargoLock = ./Cargo.lock;
+ strictDeps = true;
+ cargoArtifacts = null;
+ doCheck = false;
+ env = nativeEnv;
+ nativeBuildInputs = rustNativeInputs ++ [pkgs.makeWrapper pkgs.python3 pkgs.perl skiaGit];
+ buildInputs = nativeLibraries;
+ rsHarborCargoTomlContents = builtins.readFile ./Cargo.toml;
+ preBuild = ''
+ # skia-bindings uses the packaged rust-skia commit to select its
+ # prebuilt archive. Recreate that metadata in the Nix source tree
+ # because Cargo vendor sources do not retain the upstream Git repo.
+ mkdir -p .git/refs/heads
+ printf 'ref: refs/heads/main\n' > .git/HEAD
+ printf '%s\n' ${skiaRepositoryHash} > .git/refs/heads/main
+ mkdir -p vendor
+ rm -rf vendor/agent vendor/casement vendor/jian
+ cp -R --no-preserve=ownership ${agent} vendor/agent
+ cp -R --no-preserve=ownership ${casement} vendor/casement
+ cp -R --no-preserve=ownership ${jian} vendor/jian
+ export SKIA_BINARIES_URL=${lib.escapeShellArg nativeEnv.SKIA_BINARIES_URL}
+ export FORCE_SKIA_BINARIES_DOWNLOAD=1
+ '';
+ };
+
+ nativePackage = craneLib.buildPackage (commonArgs
+ // {
+ pname = "openpencil-native";
+ cargoBuildCommand = "cargo build --release --package op-host-desktop --package op-cli --package op-host-web-server";
+ installPhase = ''
+ runHook preInstall
+ install -Dm755 target/release/openpencil-desktop $out/bin/openpencil-desktop
+ install -Dm755 target/release/op $out/bin/op
+ install -Dm755 target/release/op-host-web-server $out/bin/op-host-web-server
+ install -Dm644 crates/op-host-desktop/assets/icon.png $out/share/icons/hicolor/1024x1024/apps/openpencil.png
+ install -Dm644 ${pkgs.writeText "openpencil.desktop" ''
+ [Desktop Entry]
+ Name=OpenPencil
+ Comment=Open-source AI-native vector design tool
+ Exec=openpencil-desktop %U
+ Terminal=false
+ Type=Application
+ Icon=openpencil
+ Categories=Graphics;
+ MimeType=application/x-openpencil;
+ ''} $out/share/applications/openpencil.desktop
+ install -Dm644 ${pkgs.writeText "openpencil.xml" ''
+
+
+
+ OpenPencil Document
+
+
+
+
+ ''} $out/share/mime/packages/openpencil.xml
+ install -Dm644 crates/op-host-desktop/assets/icon.png $out/share/pixmaps/openpencil.png
+ runHook postInstall
+ '';
+ postFixup = ''
+ for program in openpencil-desktop op op-host-web-server; do
+ wrapProgram "$out/bin/$program" \
+ --prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath runtimeLibraries}
+ done
+ '';
+ });
+
+ runtimePackage = pkgs.symlinkJoin {
+ name = "openpencil-runtime-${version}";
+ paths = [nativePackage];
+ postBuild = ''
+ install -Dm644 ${defaultDocument} "$out/share/openpencil/default.op"
+ '';
+ };
+
+ # Upstream publishes matching Linux archives for tagged releases. These
+ # outputs are deliberately separate from the source-built packages: the
+ # source build remains the default, while users with a matching release
+ # asset can avoid compiling Rust and Skia.
+ prebuiltDesktopPackage = pkgs.stdenvNoCC.mkDerivation {
+ pname = "openpencil-prebuilt";
+ version = releaseVersion;
+ src = pkgs.fetchurl {
+ url = "https://github.com/ZSeven-W/openpencil/releases/download/v${releaseVersion}/openpencil-desktop-linux-x86_64.tar.gz";
+ hash = releaseManifest.desktopHash;
+ };
+ dontUnpack = true;
+ nativeBuildInputs = [pkgs.autoPatchelfHook pkgs.makeWrapper];
+ buildInputs = prebuiltDesktopRuntimeLibraries;
+ installPhase = ''
+ runHook preInstall
+ tar -xzf "$src" -C "$TMPDIR"
+ install -Dm755 "$TMPDIR/openpencil-desktop" $out/bin/openpencil-desktop
+ install -Dm644 ${./crates/op-host-desktop/assets/icon.png} \
+ $out/share/icons/hicolor/1024x1024/apps/openpencil.png
+ install -Dm644 ${pkgs.writeText "openpencil-prebuilt.desktop" ''
+ [Desktop Entry]
+ Name=OpenPencil
+ Comment=Open-source AI-native vector design tool
+ Exec=openpencil-desktop %U
+ Terminal=false
+ Type=Application
+ Icon=openpencil
+ Categories=Graphics;
+ ''} $out/share/applications/openpencil.desktop
+ runHook postInstall
+ '';
+ postFixup = ''
+ wrapProgram $out/bin/openpencil-desktop \
+ --prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath prebuiltDesktopRuntimeLibraries}
+ '';
+ };
+
+ prebuiltCliPackage = pkgs.stdenvNoCC.mkDerivation {
+ pname = "openpencil-cli-prebuilt";
+ version = releaseVersion;
+ src = pkgs.fetchurl {
+ url = "https://github.com/ZSeven-W/openpencil/releases/download/v${releaseVersion}/op-cli-linux-x86_64.tar.gz";
+ hash = releaseManifest.cliHash;
+ };
+ dontUnpack = true;
+ nativeBuildInputs = [pkgs.autoPatchelfHook];
+ buildInputs = prebuiltCliRuntimeLibraries;
+ installPhase = ''
+ runHook preInstall
+ tar -xzf "$src" -C "$TMPDIR"
+ install -Dm755 "$TMPDIR/op" $out/bin/op
+ runHook postInstall
+ '';
+ };
+
+ wasmBindgen = rs-harbor.lib.resolveWasmBindgenCli {
+ inherit lib pkgs;
+ cargoLock = ./Cargo.lock;
+ };
+
+ wasmGate = rawName: optimizedName: ''
+ wasm_opt_help="$(${pkgs.binaryen}/bin/wasm-opt --help 2>&1 || true)"
+ wasm_opt_flags=""
+ for flag in --enable-bulk-memory --enable-bulk-memory-opt --enable-nontrapping-float-to-int; do
+ if printf '%s\n' "$wasm_opt_help" | grep -qF -- "$flag"; then
+ wasm_opt_flags="$wasm_opt_flags $flag"
+ fi
+ done
+ ${pkgs.binaryen}/bin/wasm-opt $wasm_opt_flags -Oz "$out/${rawName}" -o "$out/${optimizedName}"
+ cp "$out/${optimizedName}" "$out/${rawName}"
+ env_count="$(${pkgs.nodejs}/bin/node -e '
+ const fs = require("fs");
+ const buf = fs.readFileSync(process.argv[1]);
+ WebAssembly.compile(buf).then(mod => {
+ console.log(WebAssembly.Module.imports(mod).filter(i => i.module === "env").length);
+ }).catch(e => { console.error(e); process.exit(1); });
+ ' "$out/${rawName}")"
+ test "$env_count" = 0
+ gzip_bytes="$(${pkgs.gzip}/bin/gzip -c "$out/${rawName}" | ${pkgs.coreutils}/bin/wc -c)"
+ test "$gzip_bytes" -le 6291456
+ '';
+
+ webHostBundle = craneLib.buildPackage (commonArgs
+ // {
+ pname = "openpencil-web-bundle";
+ cargoBuildCommand = "cargo build --release --target wasm32-unknown-unknown --no-default-features --features canvaskit --package op-host-web";
+ nativeBuildInputs = commonArgs.nativeBuildInputs ++ [wasmBindgen.package pkgs.binaryen pkgs.nodejs pkgs.gzip];
+ installPhase = ''
+ runHook preInstall
+ mkdir -p $out
+ ${wasmBindgen.package}/bin/wasm-bindgen --target web --out-dir $out target/wasm32-unknown-unknown/release/op_host_web.wasm
+ cp -R crates/op-host-web/assets/canvaskit $out/canvaskit
+ ${wasmGate "op_host_web_bg.wasm" "op_host_web_bg.opt.wasm"}
+ runHook postInstall
+ '';
+ });
+
+ webSdkWasm = craneLib.buildPackage (commonArgs
+ // {
+ pname = "openpencil-web-sdk-wasm";
+ cargoBuildCommand = "cargo build --release --target wasm32-unknown-unknown --features canvaskit --package op-web-sdk";
+ nativeBuildInputs = commonArgs.nativeBuildInputs ++ [wasmBindgen.package pkgs.binaryen pkgs.nodejs pkgs.gzip];
+ installPhase = ''
+ runHook preInstall
+ mkdir -p $out
+ ${wasmBindgen.package}/bin/wasm-bindgen --target web --out-dir $out target/wasm32-unknown-unknown/release/op_web_sdk.wasm
+ ${wasmGate "op_web_sdk_bg.wasm" "op_web_sdk_bg.opt.wasm"}
+ runHook postInstall
+ '';
+ });
+
+ webServerPackage = pkgs.symlinkJoin {
+ name = "openpencil-${version}";
+ paths = [nativePackage];
+ postBuild = ''
+ mkdir -p $out/bin/web-bundle
+ cp -R ${webHostBundle}/. $out/bin/web-bundle/
+ '';
+ };
+ opCliPackage = pkgs.symlinkJoin {
+ name = "openpencil-cli-${version}";
+ paths = [nativePackage];
+ postBuild = ''
+ mkdir -p $out/bin
+ ln -sf ${nativePackage}/bin/op $out/bin/op
+ '';
+ };
+
+ defaultDocument = pkgs.writeText "openpencil-default-${version}.op" ''
+ {
+ "version": "${version}",
+ "name": "OpenPencil Nix Session",
+ "children": []
+ }
+ '';
+
+ bunToolchain = js-harbor.lib.mkBunToolchain {
+ inherit pkgs;
+ packageJson = ./packages/package.json;
+ };
+ bunDeps = js-harbor.lib.mkBunWorkspaceDeps {
+ inherit pkgs;
+ bun = bunToolchain.bun;
+ src = source;
+ packageJson = ./packages/package.json;
+ lockfile = ./packages/bun.lock;
+ version = "1.3.14";
+ installFlags = ["--cwd" "packages"];
+ # Bun's hoisted aliases are order-dependent when multiple versions
+ # are present; nested consumers already point at the correct ones.
+ postInstallNormalize = ''
+ rm -f packages/node_modules/.bun/node_modules/{string-width,strip-ansi,wrap-ansi}
+ '';
+ hash = "sha256-jaGVEagUeYrM2MzjUPvvzIF7DJwafLXJiWLWtts38SI=";
+ };
+ webSdkPackages = pkgs.stdenvNoCC.mkDerivation {
+ pname = "openpencil-web-sdk-packages";
+ inherit version;
+ src = source;
+ nativeBuildInputs = [bunToolchain.bun pkgs.coreutils pkgs.nodejs];
+ buildPhase = ''
+ runHook preBuild
+ rm -rf packages/node_modules packages/op-web-sdk/wasm
+ cp -R ${bunDeps}/packages/node_modules packages/node_modules
+ for workspace in op-web-sdk op-web-sdk-react op-web-sdk-vue; do
+ cp -R ${bunDeps}/packages/$workspace/node_modules packages/$workspace/
+ done
+ chmod -R u+w packages/node_modules
+ chmod -R u+w packages/op-web-sdk/node_modules packages/op-web-sdk-react/node_modules packages/op-web-sdk-vue/node_modules
+ patchShebangs packages/node_modules
+ find packages/node_modules -type f -exec sed -i \
+ -e 's|^#!/usr/bin/env bun$|#!${bunToolchain.bun}/bin/bun|' \
+ -e 's|^#!/usr/bin/env node$|#!${pkgs.nodejs}/bin/node|' {} +
+ mkdir -p packages/op-web-sdk/wasm
+ cp -R ${webSdkWasm}/. packages/op-web-sdk/wasm/
+ bun run --cwd packages sync-version:check
+ bun run --cwd packages lint
+ bun run --cwd packages/op-web-sdk typecheck
+ bun run --cwd packages/op-web-sdk test
+ bun run --cwd packages/op-web-sdk build
+ bun run --cwd packages/op-web-sdk-react typecheck
+ bun run --cwd packages/op-web-sdk-react test
+ bun run --cwd packages/op-web-sdk-react build
+ bun run --cwd packages/op-web-sdk-vue typecheck
+ bun run --cwd packages/op-web-sdk-vue test
+ bun run --cwd packages/op-web-sdk-vue build
+ runHook postBuild
+ '';
+ installPhase = ''
+ runHook preInstall
+ mkdir -p $out
+ (cd packages/op-web-sdk && bun pm pack --destination $out)
+ (cd packages/op-web-sdk-react && bun pm pack --destination $out)
+ (cd packages/op-web-sdk-vue && bun pm pack --destination $out)
+ runHook postInstall
+ '';
+ };
+
+ pythonEnv = py-harbor.lib.mkPythonEnv {inherit pkgs;};
+ pythonCheck =
+ pkgs.runCommand "openpencil-python-check" {
+ nativeBuildInputs = [pythonEnv];
+ } ''
+ cp -R ${source} "$TMPDIR/openpencil-source"
+ chmod -R u+w "$TMPDIR/openpencil-source"
+ cd "$TMPDIR/openpencil-source"
+ python -m compileall -q scripts tools
+ touch $out
+ '';
+
+ # Exercise the same `nix run` surface that users consume, inside a
+ # clean NixOS VM rather than only evaluating the derivations.
+ prebuiltTestFlake = pkgs.writeTextDir "flake.nix" ''
+ {
+ outputs = {self}: {
+ apps.x86_64-linux.prebuilt = {
+ type = "app";
+ program = "${prebuiltDesktopPackage}/bin/openpencil-desktop";
+ };
+ apps.x86_64-linux.prebuilt-cli = {
+ type = "app";
+ program = "${prebuiltCliPackage}/bin/op";
+ };
+ };
+ }
+ '';
+
+ prebuiltRuntimeTest = pkgs.testers.runNixOSTest ({...}: {
+ name = "openpencil-prebuilt-runtime";
+ nodes.machine = {pkgs, ...}: {
+ system.stateVersion = "25.11";
+ virtualisation.memorySize = 2048;
+ nix.settings.experimental-features = ["nix-command" "flakes"];
+ environment.etc."openpencil-test-flake".source = prebuiltTestFlake;
+ environment.variables = {
+ LIBGL_ALWAYS_SOFTWARE = "1";
+ MESA_LOADER_DRIVER_OVERRIDE = "llvmpipe";
+ GALLIUM_DRIVER = "llvmpipe";
+ };
+ environment.systemPackages = [
+ prebuiltDesktopPackage
+ prebuiltCliPackage
+ pkgs.xvfb-run
+ ];
+ };
+ testScript = ''
+ machine.succeed("nix run --offline /etc/openpencil-test-flake#prebuilt-cli -- --version | grep -F ${releaseVersion}")
+ machine.succeed("set +e; timeout 15s xvfb-run -a -s '-screen 0 1280x720x24' nix run --offline /etc/openpencil-test-flake#prebuilt >/tmp/openpencil.log 2>&1; rc=$?; test $rc -eq 0 -o $rc -eq 124; ! grep -E 'error while loading|cannot open shared object' /tmp/openpencil.log")
+ '';
+ });
+
+ runtimePrebuiltPackage = pkgs.symlinkJoin {
+ name = "openpencil-runtime-prebuilt-${version}";
+ paths = [prebuiltDesktopPackage prebuiltCliPackage];
+ nativeBuildInputs = [pkgs.makeWrapper];
+ postBuild = ''
+ rm -f "$out/bin/op"
+ makeWrapper ${prebuiltCliPackage}/bin/op "$out/bin/op" \
+ --set-default OPENPENCIL_DESKTOP_BIN "$out/bin/openpencil-desktop"
+ install -Dm644 ${defaultDocument} "$out/share/openpencil/default.op"
+ '';
+ };
+
+ runtimePrebuiltToolsCheck = pkgs.runCommand "openpencil-runtime-prebuilt-tools-${version}" {} ''
+ test -x ${runtimePrebuiltPackage}/bin/openpencil-desktop
+ test -x ${runtimePrebuiltPackage}/bin/op
+ touch "$out"
+ '';
+
+ webServerToolsCheck = pkgs.runCommand "openpencil-web-server-tools-${version}" {} ''
+ test -x ${webServerPackage}/bin/op-host-web-server
+ test -d ${webServerPackage}/bin/web-bundle
+ touch "$out"
+ '';
+
+ skillBundle = builtins.fromJSON (
+ builtins.replaceStrings
+ ["__OPENPENCIL_VERSION__"]
+ [version]
+ (builtins.readFile ./crates/op-cli/assets/skill-bundle.json)
+ );
+ skillBundleFiles =
+ builtins.mapAttrs
+ (relativePath: contents:
+ pkgs.writeText
+ "openpencil-skill-${builtins.replaceStrings ["/"] ["-"] relativePath}"
+ contents)
+ skillBundle.files;
+ skillsPackage = pkgs.runCommand "openpencil-skills-${version}" {} ''
+ mkdir -p "$out/share/skillnet/openpencil"
+ install -Dm644 ${./nix/integration/Skillnet.pkl} \
+ "$out/share/skillnet/openpencil/Skillnet.pkl"
+ ${lib.concatStringsSep "\n" (
+ lib.mapAttrsToList
+ (relativePath: source: "install -Dm644 ${source} \"$out/share/skillnet/openpencil/${relativePath}\"")
+ skillBundleFiles
+ )}
+ '';
+
+ appimage = rs-harbor.lib.mkAppImage {
+ inherit nix-appimage system version;
+ pname = "openpencil";
+ program = "${nativePackage}/bin/openpencil-desktop";
+ };
+ in {
+ packages = {
+ default = webServerPackage;
+ openpencil = webServerPackage;
+ op-cli = opCliPackage;
+ runtime = runtimePackage;
+ runtime-prebuilt = runtimePrebuiltPackage;
+ prebuilt = prebuiltDesktopPackage;
+ prebuilt-cli = prebuiltCliPackage;
+ skills = skillsPackage;
+ web-server = webServerPackage;
+ web-bundle = webHostBundle;
+ web-sdk-wasm = webSdkWasm;
+ web-sdk-packages = webSdkPackages;
+ appimage = appimage;
+ python-tools = pythonCheck;
+ };
+
+ apps = {
+ default = {
+ type = "app";
+ program = "${nativePackage}/bin/openpencil-desktop";
+ };
+ op = {
+ type = "app";
+ program = "${nativePackage}/bin/op";
+ };
+ prebuilt = {
+ type = "app";
+ program = "${prebuiltDesktopPackage}/bin/openpencil-desktop";
+ };
+ prebuilt-cli = {
+ type = "app";
+ program = "${prebuiltCliPackage}/bin/op";
+ };
+ web-server = {
+ type = "app";
+ program = "${webServerPackage}/bin/op-host-web-server";
+ };
+ integration-export = {
+ type = "app";
+ program = "${pkgs.writeShellApplication {
+ name = "openpencil-integration-export";
+ runtimeInputs = [nix-pklx.packages.${system}.pklx pkgs.coreutils];
+ text = ''
+ export SSL_CERT_FILE="${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
+ output="''${1:-nix/integration/openpencil.nix}"
+ tmp="$(mktemp)"
+ trap 'rm -f "$tmp"' EXIT
+ pklx eval nix/integration/OpenPencil.pkl -o "$tmp"
+ mv "$tmp" "$output"
+ echo "Wrote $output from nix/integration/OpenPencil.pkl"
+ '';
+ }}/bin/openpencil-integration-export";
+ };
+ };
+
+ devShells.default = pkgs.mkShell {
+ packages = [
+ toolchain.rustToolchain
+ bunToolchain.bun
+ pythonEnv
+ wasmBindgen.package
+ pkgs.binaryen
+ pkgs.nodejs
+ pkgs.gzip
+ pkgs.cargo-watch
+ pkgs.chromium
+ ];
+ nativeBuildInputs = rustNativeInputs;
+ buildInputs = nativeLibraries;
+ env = nativeEnv;
+ };
+
+ formatter = pkgs.alejandra;
+
+ checks = {
+ default = webServerPackage;
+ native = nativePackage;
+ web-bundle = webHostBundle;
+ web-sdk-wasm = webSdkWasm;
+ web-sdk-packages = webSdkPackages;
+ python = pythonCheck;
+ prebuilt = prebuiltDesktopPackage;
+ prebuilt-cli = prebuiltCliPackage;
+ prebuilt-runtime = prebuiltRuntimeTest;
+ prebuilt-runtime-tools = runtimePrebuiltToolsCheck;
+ web-server-tools = webServerToolsCheck;
+ integration-manifest =
+ pkgs.runCommand "openpencil-integration-manifest" {
+ nativeBuildInputs = [nix-pklx.packages.${system}.pklx];
+ SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
+ } ''
+ pklx eval ${./nix/integration/OpenPencil.pkl} -o "$out"
+ '';
+ integration-sidecar =
+ pkgs.runCommand "openpencil-integration-sidecar" {
+ nativeBuildInputs = [nix-pklx.packages.${system}.pklx pkgs.diffutils];
+ SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
+ } ''
+ generated="$TMPDIR/openpencil.nix"
+ pklx eval ${./nix/integration/OpenPencil.pkl} -o "$generated"
+ diff -u ${./nix/integration/openpencil.nix} "$generated"
+ touch "$out"
+ '';
+ flake-format = pkgs.runCommand "openpencil-flake-format" {} ''
+ ${pkgs.alejandra}/bin/alejandra --check ${./flake.nix}
+ touch $out
+ '';
+ };
+ };
+ };
+}
diff --git a/nix/integration/OpenPencil.pkl b/nix/integration/OpenPencil.pkl
new file mode 100644
index 000000000..6719e9bde
--- /dev/null
+++ b/nix/integration/OpenPencil.pkl
@@ -0,0 +1,118 @@
+/// Stable integration contract exported by the OpenPencil flake.
+
+class PackageOutputs {
+ sourceRuntime: String
+ prebuiltRuntime: String
+ webServer: String
+ skills: String
+}
+
+class Executables {
+ desktop: String
+ cli: String
+}
+
+class McpTransport {
+ kind: String
+ args: Listing = new Listing {}
+ urlTemplate: String? = null
+ launcherArgs: Listing = new Listing {}
+ loopbackOnly: Boolean = true
+}
+
+class HarnessAdapter {
+ format: String
+ configPath: String
+ serverKey: String
+}
+
+class Integration {
+ schemaVersion: Int
+ id: String
+ systems: Listing
+ packages: PackageOutputs
+ executables: Executables
+ documentTemplate: String
+ mcp: Mapping
+ harnesses: Mapping
+ skillnetManifest: String
+ skills: Listing
+}
+
+integration = new Integration {
+ schemaVersion = 1
+ id = "openpencil"
+ systems = new Listing { "x86_64-linux" }
+ packages = new PackageOutputs {
+ sourceRuntime = "runtime"
+ prebuiltRuntime = "runtime-prebuilt"
+ webServer = "web-server"
+ skills = "skills"
+ }
+ executables = new Executables {
+ desktop = "openpencil-desktop"
+ cli = "op"
+ }
+ documentTemplate = "share/openpencil/default.op"
+ mcp = new {
+ ["stdio"] = new McpTransport {
+ kind = "stdio"
+ args = new Listing { "--mcp"; "{document}" }
+ }
+ ["http"] = new McpTransport {
+ kind = "http"
+ urlTemplate = "http://127.0.0.1:{port}/mcp"
+ launcherArgs = new Listing { "--live-mcp"; "{port}" }
+ loopbackOnly = true
+ }
+ }
+ harnesses = new {
+ ["claude"] = new HarnessAdapter {
+ format = "json"
+ configPath = "~/.claude.json"
+ serverKey = "openpencil"
+ }
+ ["codex"] = new HarnessAdapter {
+ format = "toml"
+ configPath = "~/.codex/config.toml"
+ serverKey = "openpencil"
+ }
+ ["gemini"] = new HarnessAdapter {
+ format = "json"
+ configPath = "~/.gemini/settings.json"
+ serverKey = "openpencil"
+ }
+ ["opencode"] = new HarnessAdapter {
+ format = "json"
+ configPath = "~/.opencode/config.json"
+ serverKey = "openpencil"
+ }
+ ["kiro"] = new HarnessAdapter {
+ format = "json"
+ configPath = "~/.kiro/settings.json"
+ serverKey = "openpencil"
+ }
+ ["copilot"] = new HarnessAdapter {
+ format = "json"
+ configPath = "~/.config/github-copilot/mcp.json"
+ serverKey = "openpencil"
+ }
+ ["antigravity"] = new HarnessAdapter {
+ format = "json"
+ configPath = "~/.gemini/config/mcp_config.json"
+ serverKey = "openpencil"
+ }
+ ["grok-build"] = new HarnessAdapter {
+ format = "toml"
+ configPath = "~/.grok/config.toml"
+ serverKey = "openpencil"
+ }
+ ["hermes"] = new HarnessAdapter {
+ format = "nix"
+ configPath = "services.infernix.hermes-agent.mcpServers"
+ serverKey = "openpencil"
+ }
+ }
+ skillnetManifest = "share/skillnet/openpencil/Skillnet.pkl"
+ skills = new Listing { "openpencil-design" }
+}
diff --git a/nix/integration/README.md b/nix/integration/README.md
new file mode 100644
index 000000000..96508f91f
--- /dev/null
+++ b/nix/integration/README.md
@@ -0,0 +1,21 @@
+# OpenPencil Nix integration surface
+
+The OpenPencil flake exports a versioned Pkl contract at
+`nix/integration/OpenPencil.pkl` and the generated Nix sidecar at
+`nix/integration/openpencil.nix` (`lib.integrationManifest`). Downstream flakes
+should consume the sidecar rather than duplicating executable names, MCP
+transport arguments, or harness config paths.
+
+For each supported system the contract names these package outputs:
+
+- `runtime`: source-built desktop and CLI runtime;
+- `runtime-prebuilt`: the matching release binaries, wrapped so `op` can find
+ the desktop executable (exports both `openpencil-desktop` and `op`);
+- `web-server`: the source-built `op-host-web-server` plus its web bundle;
+- `skills`: an immutable Skillnet bundle containing `Skillnet.pkl` and the
+ OpenPencil design skill.
+
+The default MCP adapter is per-session stdio (`--mcp `). The HTTP
+adapter is loopback-only and is intended for an explicitly enabled live-canvas
+service (`--live-mcp `). Consumers should merge the generated adapter
+entry into their harness config atomically and preserve unrelated keys.
diff --git a/nix/integration/Skillnet.pkl b/nix/integration/Skillnet.pkl
new file mode 100644
index 000000000..671dec249
--- /dev/null
+++ b/nix/integration/Skillnet.pkl
@@ -0,0 +1,15 @@
+/// Skillnet manifest shipped in the immutable OpenPencil skills bundle.
+
+class Skill {
+ role: String = "entrypoint"
+ dependencies: Listing = new Listing {}
+ source: String? = null
+}
+
+schemaVersion = 1
+skills: Mapping = new {
+ ["openpencil-design"] = new Skill {
+ role = "entrypoint"
+ source = "skills/openpencil-design"
+ }
+}
diff --git a/nix/integration/openpencil.nix b/nix/integration/openpencil.nix
new file mode 100644
index 000000000..3c97810ed
--- /dev/null
+++ b/nix/integration/openpencil.nix
@@ -0,0 +1,11 @@
+{ integration = { __pkl_class = "Integration"; schemaVersion = 1; id = "openpencil"; systems = [
+ "x86_64-linux"
+ ]; packages = { __pkl_class = "PackageOutputs"; sourceRuntime = "runtime"; prebuiltRuntime = "runtime-prebuilt"; webServer = "web-server"; skills = "skills"; }; executables = { __pkl_class = "Executables"; desktop = "openpencil-desktop"; cli = "op"; }; documentTemplate = "share/openpencil/default.op"; mcp = { stdio = { __pkl_class = "McpTransport"; kind = "stdio"; args = [
+ "--mcp"
+ "{document}"
+ ]; urlTemplate = null; launcherArgs = [ ]; loopbackOnly = true; }; http = { __pkl_class = "McpTransport"; kind = "http"; args = [ ]; urlTemplate = "http://127.0.0.1:{port}/mcp"; launcherArgs = [
+ "--live-mcp"
+ "{port}"
+ ]; loopbackOnly = true; }; }; harnesses = { claude = { __pkl_class = "HarnessAdapter"; format = "json"; configPath = "~/.claude.json"; serverKey = "openpencil"; }; codex = { __pkl_class = "HarnessAdapter"; format = "toml"; configPath = "~/.codex/config.toml"; serverKey = "openpencil"; }; gemini = { __pkl_class = "HarnessAdapter"; format = "json"; configPath = "~/.gemini/settings.json"; serverKey = "openpencil"; }; opencode = { __pkl_class = "HarnessAdapter"; format = "json"; configPath = "~/.opencode/config.json"; serverKey = "openpencil"; }; kiro = { __pkl_class = "HarnessAdapter"; format = "json"; configPath = "~/.kiro/settings.json"; serverKey = "openpencil"; }; copilot = { __pkl_class = "HarnessAdapter"; format = "json"; configPath = "~/.config/github-copilot/mcp.json"; serverKey = "openpencil"; }; antigravity = { __pkl_class = "HarnessAdapter"; format = "json"; configPath = "~/.gemini/config/mcp_config.json"; serverKey = "openpencil"; }; grok-build = { __pkl_class = "HarnessAdapter"; format = "toml"; configPath = "~/.grok/config.toml"; serverKey = "openpencil"; }; hermes = { __pkl_class = "HarnessAdapter"; format = "nix"; configPath = "services.infernix.hermes-agent.mcpServers"; serverKey = "openpencil"; }; }; skillnetManifest = "share/skillnet/openpencil/Skillnet.pkl"; skills = [
+ "openpencil-design"
+ ]; }; }
\ No newline at end of file
diff --git a/nix/release-manifest.json b/nix/release-manifest.json
new file mode 100644
index 000000000..ea2666819
--- /dev/null
+++ b/nix/release-manifest.json
@@ -0,0 +1,5 @@
+{
+ "version": "0.8.2",
+ "desktopHash": "sha256-btwzBgYRuaKkMqRxbz0uDpJTfnaYqWXl16WA82sCyCI=",
+ "cliHash": "sha256-rv+xEUhX57gQ5mzZ7JJ/qIPd4Ms+vwpu4miR4oiNIKI="
+}
diff --git a/packages/package.json b/packages/package.json
index ff18a7a97..d345a6c1a 100644
--- a/packages/package.json
+++ b/packages/package.json
@@ -14,6 +14,7 @@
"op-web-sdk-vue"
],
"type": "module",
+ "packageManager": "bun@1.3.14",
"scripts": {
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",