diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee245cdd..f941e55c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,46 @@ HTTPS or localhost, because it uses the Origin Private File System. Windows from Linux: `cargo xwin build --release --target x86_64-pc-windows-msvc -p solvecraft`. +### NixOS: `nix develop` + +NixOS has no `/usr/lib`, so nothing is on `LD_LIBRARY_PATH` and a plain `cargo run` finds none of +the libraries the app loads at run time: the Wayland ones winit opens the window through, the EGL +loader wgpu renders through, and libdbus-1, which rfd's file dialogs reach xdg-desktop-portal +through (a dialog then never appears and the pick returns no path): + +``` +ERROR solvecraft: winit EventLoopError: ... The wayland library could not be loaded +ERROR eframe::native::run: Exiting because of error: WGPU error: Failed to create surface for any enabled backend: {} +ERROR rfd::backend::xdg_desktop_portal::portal::libdbus: Can't connect to a portal: libdbus-1.so not found +``` + +`nix develop` (the [`flake.nix`](flake.nix) in the repository root) puts the Wayland, X11, +EGL-loader and D-Bus libraries on `LD_LIBRARY_PATH`, so `cargo run --release -p solvecraft` works +from that shell as on any other distribution, file dialogs included. `nix run` builds the app and +wraps the binary the same way. The Mesa drivers stay the system's, from `/run/opengl-driver` +(`hardware.graphics.enable`): that is the driver stack the compositor runs on, and wgpu reaches the +integrated GPU through the EGL loader. The shell deliberately brings no Rust toolchain — the one on +`PATH` keeps its build cache, where a second `rustc` would make every build recompile the workspace +from scratch; `nix develop .#rust` is there for a machine with no Rust at all. + +**Anti-aliasing on the OpenGL backend.** NixOS keeps the Vulkan ICD descriptions +(`/run/opengl-driver/share/vulkan/icd.d`) off the Vulkan loader's search path, so wgpu picks its +OpenGL backend on the integrated GPU. On that backend any multisampling above 1× — the default is +4× — draws a black window: the UI still lays out and the control channel still answers, but every +frame, `ui.screenshot` included, comes out empty. Set **Preferences ▸ Graphics ▸ Anti-aliasing** to +1× and restart (the setting applies on the next start), or write it before the first run, since a +black window is no way to reach the dialog: + +```sh +mkdir -p ~/.local/share/solvecraft +echo '{"msaa": 1}' > ~/.local/share/solvecraft/graphics.json +``` + +`apps/solvecraft/src/main.rs` reads that file (`graphics_path()`, eframe's storage dir) before the +window opens; 2× and 4× are the same black window. The Vulkan backend is no way out of it either: +`libvulkan.so.1` is not on `LD_LIBRARY_PATH`, so `WGPU_BACKEND=vulkan` exits with a +`CreateSurfaceError` instead. + ## How the code is laid out The [README](README.md#architecture) has the layer table. The short version: diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..915f6ef3 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1791594108, + "narHash": "sha256-XK4KceonkwD8LuMAt/vNrz982mXJM5jeHAxMwsUXXAQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6510408d147d8c5e6d76043e899e9251756f6a75", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..6cab8489 --- /dev/null +++ b/flake.nix @@ -0,0 +1,133 @@ +{ + description = "SolveCraft — native app development shell and package for Linux"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = + { self, nixpkgs }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); + + # Why this shell exists. winit opens the window by dlopen'ing the Wayland client libraries, + # wgpu renders through the EGL loader (libEGL.so.1), and rfd's file dialogs reach + # xdg-desktop-portal through libdbus-1; a plain `cargo run` under NixOS finds none of them, + # because there is no /usr/lib and nothing puts them on LD_LIBRARY_PATH: + # winit EventLoopError: The wayland library could not be loaded + # eframe: WGPU error: Failed to create surface for any enabled backend: {} + # rfd::backend::xdg_desktop_portal::portal::libdbus: Can't connect to a portal: libdbus-1.so not found + # The Mesa drivers themselves — and the EGL vendor / Vulkan ICD descriptions that name them — + # come from the system in /run/opengl-driver (hardware.graphics.enable), not from here, since + # that is the driver stack the compositor runs on. Verified on an Intel LNL iGPU under + # Wayland: wgpu picks the GL backend through this loader and draws. + # The X11 libraries are the ones the app actually uses on a Wayland desktop: with `DISPLAY` + # set and `SOLVECRAFT_WAYLAND` unset, winit runs through XWayland (file drag and drop exists + # only there). The Wayland ones are for `SOLVECRAFT_WAYLAND=1`, and both are needed under + # `xvfb-run` on a machine with no display at all. + runtimeLibs = + pkgs: + (with pkgs; [ + wayland + libxkbcommon + libglvnd + dbus + ]) + ++ (with pkgs; [ + # xorg.libX11 and friends are deprecated aliases in current nixpkgs. + libx11 + libxcursor + libxi + libxrandr + libxrender + libxext + libxcb + ]); + driverLibDir = "/run/opengl-driver/lib"; + libraryPath = pkgs: "${pkgs.lib.makeLibraryPath (runtimeLibs pkgs)}:${driverLibDir}"; + + shellHook = '' + if [ ! -d ${driverLibDir} ]; then + echo "solvecraft: ${driverLibDir} is missing — set hardware.graphics.enable = true so the Mesa drivers and their EGL vendor descriptions exist." >&2 + fi + command -v cc >/dev/null || echo "solvecraft: no C linker on PATH, cargo cannot link — use nix develop .#rust" >&2 + echo "solvecraft: cargo run --release -p solvecraft -- --sample" + ''; + in + { + devShells = forAllSystems (pkgs: { + # The shell to work in. It brings no toolchain of its own, Rust or C: the ones already on + # PATH (a system package, rustup, nixpkgs) keep their build cache and their linker, where a + # second rustc — or mkShell's second gcc, with another glibc behind it — would make every + # `cargo build` produce binaries from somewhere else. + default = pkgs.mkShellNoCC { + name = "solvecraft"; + LD_LIBRARY_PATH = libraryPath pkgs; + inherit shellHook; + }; + + # For a machine with no Rust at all: `nix develop .#rust`. Its own target directory, so + # switching between this toolchain and the system one does not thrash the cache. + rust = pkgs.mkShell { + name = "solvecraft-rust"; + packages = [ + pkgs.cargo + pkgs.rustc + pkgs.rustfmt + pkgs.clippy + ]; + LD_LIBRARY_PATH = libraryPath pkgs; + # Under target/, which .gitignore already covers. + CARGO_TARGET_DIR = "target/nix"; + shellHook = '' + ${shellHook} + echo "solvecraft: $(rustc --version), building into target/nix" + ''; + }; + }); + + packages = forAllSystems (pkgs: { + default = pkgs.rustPlatform.buildRustPackage { + pname = "solvecraft"; + version = (pkgs.lib.importTOML ./Cargo.toml).workspace.package.version; + src = self; + + cargoLock.lockFile = ./Cargo.lock; + # Build and install the app alone: the workspace also holds the CLI, the web app, xtask + # and the crates only the test suites need. + cargoBuildFlags = [ + "-p" + "solvecraft" + ]; + cargoInstallFlags = [ + "-p" + "solvecraft" + ]; + # `cargo xtask ci` is the test gate (it wants a wasm target and a GPU); the sandbox has + # neither. + doCheck = false; + + nativeBuildInputs = [ pkgs.makeWrapper ]; + buildInputs = runtimeLibs pkgs; + + # The app reaches the window through dlopen'd libraries, so the packaged binary needs the + # same LD_LIBRARY_PATH the shell exports. + postFixup = '' + wrapProgram $out/bin/solvecraft --prefix LD_LIBRARY_PATH : "${libraryPath pkgs}" + ''; + + meta = { + description = "Rust-native parametric 3D CAD app"; + homepage = "https://github.com/bherbruck/solvecraft"; + license = with pkgs.lib.licenses; [ + mit + asl20 + ]; + mainProgram = "solvecraft"; + }; + }; + }); + }; +}