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 .",