diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index b333c38..0278db7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -*.epub \ No newline at end of file +*.epub +.direnv +result +target/** diff --git a/Justfile b/Justfile index a4148ce..bbc020f 100644 --- a/Justfile +++ b/Justfile @@ -3,6 +3,11 @@ gen-bindings: cargo test -p bene-epub export_bindings cp -r crates/bene-epub/bindings ../js/packages/bene-types/src/ +[working-directory: 'js'] +build: gen-bindings + depot -p bene-reader build + depot -p bene-desktop build + [working-directory: 'rs'] build-native: gen-bindings cargo tauri build diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..a1140ac --- /dev/null +++ b/flake.lock @@ -0,0 +1,137 @@ +{ + "nodes": { + "depot": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1762836243, + "narHash": "sha256-6pMVEhW50TFAR86qp2EeYLKbmr5u9penEb9JeKN+/NI=", + "owner": "cognitive-engineering-lab", + "repo": "depot", + "rev": "33c9bf77859bc5e61181d4600ed20521a942b3d3", + "type": "github" + }, + "original": { + "owner": "cognitive-engineering-lab", + "repo": "depot", + "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_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" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1763966396, + "narHash": "sha256-6eeL1YPcY1MV3DDStIDIdy/zZCDKgHdkCmsrLJFiZf0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5ae3b07d8d6527c42f17c876e404993199144b6a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "depot": "depot", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1764038373, + "narHash": "sha256-M6w2wNBRelcavoDAyFL2iO4NeWknD40ASkH1S3C0YGM=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "ab3536fe850211a96673c6ffb2cb88aab8071cc9", + "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_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..53daf8b --- /dev/null +++ b/flake.nix @@ -0,0 +1,69 @@ +# Bene - An EPUB reading system +# +# Usage: +# nix build - Build using pre-built binary from GitHub releases (fast) +# nix build .#from-source - Build from source (slow, for testing patches) +# nix develop - Enter development shell with Rust, Node, pnpm, depot +# nix run -- /path/to.epub - Run the reader directly +# +# The default package downloads the .deb from GitHub releases and patches it +# for NixOS using autoPatchelfHook. Only x86_64-linux is supported for the +# binary package. +# +# To update to a new release: +# 1. Update version in nix/bene-bin.nix +# 2. Run: nix-prefetch-url https://github.com/nota-lang/bene/releases/download/v/Bene.Reader__amd64.deb +# 3. Convert hash: nix hash convert --hash-algo sha256 +# 4. Update hash in nix/bene-bin.nix +# +{ + description = "Bene - An EPUB reading system with Rust backend and JavaScript frontend"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + depot = { + url = "github:cognitive-engineering-lab/depot"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, flake-utils, rust-overlay, depot }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + depotPkg = depot.packages.${system}.default; + + src = pkgs.lib.fileset.toSource { + root = ./.; + fileset = pkgs.lib.fileset.gitTracked ./.; + }; + in + { + # Default package: Pre-built binary (fast, recommended) + packages.default = pkgs.callPackage ./nix/bene-bin.nix { }; + + # From-source build (for development/testing patches) + packages.from-source = pkgs.callPackage ./nix/bene-src.nix { + inherit rustToolchain depotPkg src; + pnpm = pkgs.pnpm_9; + }; + + # Development shell with all tools + devShells.default = pkgs.callPackage ./nix/devshell.nix { + inherit rustToolchain depotPkg; + }; + + # Formatter for `nix fmt` + formatter = pkgs.nixpkgs-fmt; + }); +} diff --git a/nix/bene-bin.nix b/nix/bene-bin.nix new file mode 100644 index 0000000..95b7246 --- /dev/null +++ b/nix/bene-bin.nix @@ -0,0 +1,87 @@ +# Pre-built binary package for Bene +# Downloads the .deb from GitHub releases and patches it for NixOS +{ lib +, stdenv +, fetchurl +, dpkg +, autoPatchelfHook +, wrapGAppsHook3 +, gtk3 +, webkitgtk_4_1 +, libsoup_3 +, glib +, glib-networking +, openssl +, libayatana-appindicator +}: + +stdenv.mkDerivation rec { + pname = "bene"; + version = "0.1.3"; + + src = fetchurl { + url = "https://github.com/nota-lang/bene/releases/download/v${version}/Bene.Reader_${version}_amd64.deb"; + hash = "sha256-zO4WlKmDkZc/bIPB+3mcTcj1xfUbrVo4sHGNvaoGjrI="; + }; + + unpackCmd = "${dpkg}/bin/dpkg-deb -x $curSrc ."; + sourceRoot = "."; + + nativeBuildInputs = [ + autoPatchelfHook + wrapGAppsHook3 + dpkg + ]; + + buildInputs = [ + gtk3 + webkitgtk_4_1 + libsoup_3 + glib + glib-networking + openssl + libayatana-appindicator + ]; + + dontConfigure = true; + dontBuild = true; + + installPhase = '' + runHook preInstall + + mkdir -p $out/bin $out/lib $out/share + + # Install the main binary (upstream names it bene-app) + install -Dm755 usr/bin/bene-app $out/bin/bene + + # Install bundled resources (bene-reader assets) + if [ -d "usr/lib/Bene Reader" ]; then + cp -r "usr/lib/Bene Reader" "$out/lib/" + fi + + # Install desktop file + if [ -d usr/share/applications ]; then + cp -r usr/share/applications $out/share/ + fi + + # Install icons + if [ -d usr/share/icons ]; then + cp -r usr/share/icons $out/share/ + fi + + runHook postInstall + ''; + + postFixup = '' + wrapProgram $out/bin/bene \ + --prefix GIO_EXTRA_MODULES : "${glib-networking}/lib/gio/modules" + ''; + + meta = with lib; { + description = "An EPUB reading system"; + homepage = "https://github.com/nota-lang/bene"; + license = with licenses; [ mit asl20 ]; + platforms = [ "x86_64-linux" ]; + mainProgram = "bene"; + }; +} diff --git a/nix/bene-src.nix b/nix/bene-src.nix new file mode 100644 index 0000000..8f79ee6 --- /dev/null +++ b/nix/bene-src.nix @@ -0,0 +1,164 @@ +# Build Bene from source +# This is the original build process, kept for development and testing patches +{ lib +, stdenv +, rustToolchain +, depotPkg +, pnpm +, nodejs_22 +, cargo-tauri +, rustPlatform +, pkg-config +, wrapGAppsHook4 +, gtk3 +, webkitgtk_4_1 +, libsoup_3 +, dpkg +, src +}: + +let + pnpmDeps = pnpm.fetchDeps { + pname = "bene-frontend-deps"; + version = "0.1.0"; + src = src + "/js"; + hash = "sha256-xadUV/A8rWPc0yHewU/BQxgl6gkgvf4OQPsPghbaYKA="; + fetcherVersion = 2; + }; +in +stdenv.mkDerivation { + pname = "bene"; + version = "0.1.0"; + + inherit src; + + nativeBuildInputs = [ + rustToolchain + cargo-tauri + rustPlatform.cargoSetupHook + nodejs_22 + pnpm + pnpm.configHook + depotPkg + pkg-config + wrapGAppsHook4 + ]; + + buildInputs = [ + gtk3 + webkitgtk_4_1 + libsoup_3 + ]; + + inherit pnpmDeps; + + cargoDeps = rustPlatform.fetchCargoVendor { + src = src + "/rs"; + name = "bene-cargo-deps"; + hash = "sha256-y42Jy1aTZv9/syrN207WiJiI7J3RCN7EFkP8r/OVd+g="; + }; + + cargoRoot = "rs"; + pnpmRoot = "js"; + + preBuild = '' + export HOME=$TMPDIR + export PNPM_HOME=$TMPDIR/.pnpm + mkdir -p $PNPM_HOME + + # Step 1: Generate TypeScript bindings from Rust + echo "Generating TypeScript bindings..." + cd rs + cargo test -p bene-epub export_bindings --release + cp -r crates/bene-epub/bindings ../js/packages/bene-types/src/ + cd .. + + # Step 2: Install pnpm dependencies (offline mode uses pre-fetched store) + echo "Installing pnpm dependencies..." + cd js + pnpm install --frozen-lockfile --offline + + # Step 3: Build frontend with depot + echo "Building frontend with depot..." + depot -p bene-desktop build --release + + # Verify frontend build succeeded + if [ ! -d packages/bene-desktop/dist ]; then + echo "Error: bene-desktop dist directory not found" + exit 1 + fi + + if [ ! -d packages/bene-reader/dist ]; then + echo "Error: bene-reader dist directory not found" + exit 1 + fi + + cd .. + + # Step 4: Disable Tauri's beforeBuildCommand since frontend is already built + echo "Modifying Tauri.toml to skip beforeBuildCommand..." + cd rs/crates/bene-app + grep -v "^beforeBuildCommand" Tauri.toml > Tauri.toml.nix + mv Tauri.toml.nix Tauri.toml + cd ../../.. + ''; + + buildPhase = '' + runHook preBuild + + # Build Tauri desktop application + echo "Building Tauri application..." + cd rs + cargo tauri build --bundles deb + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + cd .. + + # Install the deb package + mkdir -p $out/share/packages + find rs/target/release/bundle/deb -name "*.deb" -exec cp {} $out/share/packages/ \; + + # Extract and install the binary from the deb + mkdir -p $out/bin + cd rs/target/release/bundle/deb + ${dpkg}/bin/dpkg-deb -x *.deb $TMPDIR/deb-extract + + find $TMPDIR/deb-extract/usr/bin -type f -executable -exec cp {} $out/bin/bene \; || true + + # Install bundled resources + mkdir -p "$out/lib/Bene Reader" + if [ -d "$TMPDIR/deb-extract/usr/lib/Bene Reader" ]; then + cp -r "$TMPDIR/deb-extract/usr/lib/Bene Reader"/* "$out/lib/Bene Reader/" + else + echo "WARNING: Resources directory not found in deb package" + exit 1 + fi + + # Install desktop file + mkdir -p $out/share/applications + if [ -d $TMPDIR/deb-extract/usr/share/applications ]; then + find $TMPDIR/deb-extract/usr/share/applications -name "*.desktop" -exec cp {} $out/share/applications/ \; + fi + + # Install icons + if [ -d $TMPDIR/deb-extract/usr/share/icons ]; then + mkdir -p $out/share/icons + cp -r $TMPDIR/deb-extract/usr/share/icons/* $out/share/icons/ || true + fi + + runHook postInstall + ''; + + meta = with lib; { + description = "An EPUB reading system"; + homepage = "https://github.com/nota-lang/bene"; + license = with licenses; [ mit asl20 ]; + platforms = platforms.linux; + mainProgram = "bene"; + }; +} diff --git a/nix/devshell.nix b/nix/devshell.nix new file mode 100644 index 0000000..f0b7c86 --- /dev/null +++ b/nix/devshell.nix @@ -0,0 +1,76 @@ +# Development shell for Bene +{ mkShell +, lib +, rustToolchain +, depotPkg +, cargo-tauri +, wasm-pack +, nodejs_22 +, pnpm +, just +, rust-analyzer +, pkg-config +, wrapGAppsHook4 +, gtk3 +, webkitgtk_4_1 +, libsoup_3 +}: + +let + tauriBuildInputs = [ + gtk3 + webkitgtk_4_1 + libsoup_3 + ]; + + tauriNativeBuildInputs = [ + pkg-config + wrapGAppsHook4 + ]; +in +mkShell { + buildInputs = [ + # Rust toolchain (from rust-toolchain.toml) + rustToolchain + + # Rust tools + cargo-tauri + wasm-pack + + # JavaScript tools + nodejs_22 + pnpm + depotPkg + + # Build tools + just + + # Development tools + rust-analyzer + + ] ++ tauriBuildInputs ++ tauriNativeBuildInputs; + + # Environment setup + RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; + LD_LIBRARY_PATH = lib.makeLibraryPath tauriBuildInputs; + + shellHook = '' + echo "Bene development environment" + echo "" + echo "Versions:" + echo " Rust: $(rustc --version)" + echo " Node: $(node --version)" + echo " pnpm: $(pnpm --version)" + echo " depot: $(depot --version)" + echo "" + echo "Available commands:" + echo " just build-native - Build Tauri desktop app" + echo " just build-wasm - Build web app" + echo " just dev-native - Run desktop app in dev mode" + echo " just test - Run all tests" + echo " just gen-bindings - Generate TypeScript bindings" + echo "" + echo "Targets available:" + rustup target list --installed | grep -E '(wasm|x86)' + ''; +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..c38c8a8 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] +targets = ["wasm32-unknown-unknown"]