From 5d222920728eeaabc232dd52cd1e8f4405c91fbc Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 7 Mar 2026 14:26:19 +0100 Subject: [PATCH] Replace `npm` build script with new `cargo run` tool (#3832) * move nix flake to root * cargo run tool * use thiserror in third-party-licenses tool * prefere panic over exit * Add automatic dependency check to cargo run tool * Skip dependecies that are not needed for the current task * Fixup * Fixup * fix windows * Fixup * improve usage text * Fix linux bundle * add graphen-cli * fix build profile * fix * release profile should not include debug infos * Review * remove profiling profile was redundent with release * rename to cargo-run tool * improve consistency * rename deps to requirements * fix * return success when showing usage --- .envrc | 2 +- .github/workflows/build-linux-bundle.yml | 8 +- .github/workflows/build-mac-bundle.yml | 2 +- .github/workflows/build-nix-package.yml | 2 +- .github/workflows/build-production.yml | 4 +- .github/workflows/build-win-bundle.yml | 2 +- .github/workflows/ci.yml | 6 +- .github/workflows/comment-!build-commands.yml | 19 +- .github/workflows/deploy-master.yml | 6 +- .github/workflows/provide-shaders.yml | 2 +- .nix/default.nix | 81 ++++++++ .nix/flake.nix | 110 ---------- .nix/shell.nix | 28 --- Cargo.lock | 8 + Cargo.toml | 7 +- desktop/bundle/src/common.rs | 7 +- desktop/bundle/src/linux.rs | 13 +- desktop/bundle/src/mac.rs | 8 +- desktop/bundle/src/win.rs | 7 +- desktop/src/cef/context/builder.rs | 21 +- desktop/src/lib.rs | 23 +- .nix/flake.lock => flake.lock | 15 -- flake.nix | 25 +++ frontend/package.json | 4 - frontend/wasm/Cargo.toml | 8 - node-graph/graph-craft/.cargo/config.toml | 3 - node-graph/nodes/raster/shaders/build.rs | 2 +- package-lock.json | 6 - package.json | 20 -- tools/cargo-run/Cargo.toml | 11 + tools/cargo-run/src/lib.rs | 104 ++++++++++ tools/cargo-run/src/main.rs | 99 +++++++++ tools/cargo-run/src/requirements.rs | 196 ++++++++++++++++++ tools/third-party-licenses/Cargo.toml | 1 + tools/third-party-licenses/src/cargo.rs | 25 +-- tools/third-party-licenses/src/cef.rs | 43 ++-- tools/third-party-licenses/src/main.rs | 80 ++++--- tools/third-party-licenses/src/npm.rs | 24 +-- .../volunteer/guide/project-setup/_index.md | 36 +--- 39 files changed, 664 insertions(+), 404 deletions(-) create mode 100644 .nix/default.nix delete mode 100644 .nix/flake.nix delete mode 100644 .nix/shell.nix rename .nix/flake.lock => flake.lock (72%) create mode 100644 flake.nix delete mode 100644 node-graph/graph-craft/.cargo/config.toml delete mode 100644 package-lock.json delete mode 100644 package.json create mode 100644 tools/cargo-run/Cargo.toml create mode 100644 tools/cargo-run/src/lib.rs create mode 100644 tools/cargo-run/src/main.rs create mode 100644 tools/cargo-run/src/requirements.rs diff --git a/.envrc b/.envrc index 930f30ba..3550a30f 100644 --- a/.envrc +++ b/.envrc @@ -1 +1 @@ -use flake .nix +use flake diff --git a/.github/workflows/build-linux-bundle.yml b/.github/workflows/build-linux-bundle.yml index 9ca2334a..d5584f35 100644 --- a/.github/workflows/build-linux-bundle.yml +++ b/.github/workflows/build-linux-bundle.yml @@ -25,7 +25,7 @@ jobs: run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache - name: Build Nix Package - run: nix build .nix --no-link --print-out-paths + run: nix build --no-link --print-out-paths - name: Push to Nix Cache if: github.ref == 'refs/heads/master' || inputs.push_to_cache == true @@ -33,10 +33,10 @@ jobs: NIX_CACHE_AUTH_TOKEN: ${{ secrets.NIX_CACHE_AUTH_TOKEN }} run: | nix run nixpkgs#cachix -- authtoken $NIX_CACHE_AUTH_TOKEN - nix build .nix --no-link --print-out-paths | nix run nixpkgs#cachix -- push graphite + nix build --no-link --print-out-paths | nix run nixpkgs#cachix -- push graphite - name: Build Linux Bundle - run: nix build .nix#graphite-bundle.tar.xz && cp ./result ./graphite-linux-bundle.tar.xz + run: nix build .#graphite-bundle.tar.xz && cp ./result ./graphite-linux-bundle.tar.xz - name: Upload Linux Bundle uses: actions/upload-artifact@v4 @@ -53,7 +53,7 @@ jobs: - name: Build Flatpak run: | - nix build .nix#graphite-flatpak-manifest + nix build .#graphite-flatpak-manifest rm -rf .flatpak mkdir -p .flatpak diff --git a/.github/workflows/build-mac-bundle.yml b/.github/workflows/build-mac-bundle.yml index 826e5f0a..b11eba79 100644 --- a/.github/workflows/build-mac-bundle.yml +++ b/.github/workflows/build-mac-bundle.yml @@ -67,7 +67,7 @@ jobs: - name: Build Mac Bundle env: CARGO_TERM_COLOR: always - run: npm run build-desktop + run: cargo run build desktop - name: Stage Artifacts shell: bash diff --git a/.github/workflows/build-nix-package.yml b/.github/workflows/build-nix-package.yml index 2da86b2d..45669d03 100644 --- a/.github/workflows/build-nix-package.yml +++ b/.github/workflows/build-nix-package.yml @@ -14,4 +14,4 @@ jobs: - uses: DeterminateSystems/magic-nix-cache-action@main - name: Build Nix Package Dev - run: nix build .nix#graphite-dev --print-build-logs + run: nix build .#graphite-dev --print-build-logs diff --git a/.github/workflows/build-production.yml b/.github/workflows/build-production.yml index 1be2e3e9..eae2aca9 100644 --- a/.github/workflows/build-production.yml +++ b/.github/workflows/build-production.yml @@ -52,9 +52,7 @@ jobs: - name: 🌐 Build Graphite web code env: NODE_ENV: production - run: | - cd frontend - mold -run npm run build + run: mold -run cargo run build web - name: 📤 Publish to Cloudflare Pages id: cloudflare diff --git a/.github/workflows/build-win-bundle.yml b/.github/workflows/build-win-bundle.yml index ee72140d..720bdb63 100644 --- a/.github/workflows/build-win-bundle.yml +++ b/.github/workflows/build-win-bundle.yml @@ -73,7 +73,7 @@ jobs: shell: bash # `cargo-about` refuses to run in powershell env: CARGO_TERM_COLOR: always - run: npm run build-desktop + run: cargo run build desktop - name: Stage Artifacts shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63621a0f..4985abfe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,7 @@ jobs: if: steps.skip-check.outputs.skip-check != 'true' uses: actions/setup-node@v4 with: - node-version: 'latest' + node-version: "latest" - name: 🚧 Install build dependencies if: steps.skip-check.outputs.skip-check != 'true' @@ -98,9 +98,7 @@ jobs: if: steps.skip-check.outputs.skip-check != 'true' env: NODE_ENV: production - run: | - cd frontend - mold -run npm run build + run: mold -run cargo run build web - name: 📤 Publish to Cloudflare Pages if: steps.skip-check.outputs.skip-check != 'true' diff --git a/.github/workflows/comment-!build-commands.yml b/.github/workflows/comment-!build-commands.yml index f2ec3217..0224b722 100644 --- a/.github/workflows/comment-!build-commands.yml +++ b/.github/workflows/comment-!build-commands.yml @@ -1,7 +1,6 @@ # USAGE: # After reviewing the code, core team members may comment on a PR with the exact text: -# - `!build-dev` to build with debug symbols and optimizations disabled -# - `!build-profiling` to build with debug symbols and optimizations enabled +# - `!build-debug` to build with debug symbols and optimizations disabled # - `!build` to build without debug symbols and optimizations enabled # The comment may not contain any other text, not even whitespace or newlines. name: "!build PR Command" @@ -21,7 +20,7 @@ jobs: if: > github.event.issue.pull_request && github.event.comment.author_association == 'MEMBER' && - (github.event.comment.body == '!build-dev' || github.event.comment.body == '!build-profiling' || github.event.comment.body == '!build') + (github.event.comment.body == '!build-debug' || github.event.comment.body == '!build') runs-on: self-hosted permissions: contents: read @@ -82,14 +81,12 @@ jobs: - name: ⌨ Set build command based on comment id: build_command run: | - if [[ "${{ github.event.comment.body }}" == "!build-dev" ]]; then - echo "command=build-dev" >> $GITHUB_OUTPUT - elif [[ "${{ github.event.comment.body }}" == "!build-profiling" ]]; then - echo "command=build-profiling" >> $GITHUB_OUTPUT + if [[ "${{ github.event.comment.body }}" == "!build-debug" ]]; then + echo "command=build web debug" >> $GITHUB_OUTPUT elif [[ "${{ github.event.comment.body }}" == "!build" ]]; then - echo "command=build" >> $GITHUB_OUTPUT + echo "command=build web" >> $GITHUB_OUTPUT else - echo "Failed to detect if the build command written in the comment should have been '!build-dev', '!build-profiling', or '!build'" >> $GITHUB_OUTPUT + echo "Failed to detect if the build command written in the comment should have been '!build-debug', or '!build'" >> $GITHUB_OUTPUT fi - name: 💬 Comment Actions run link @@ -108,9 +105,7 @@ jobs: env: NODE_ENV: production if: ${{ success() || failure()}} - run: | - cd frontend - mold -run npm run ${{ steps.build_command.outputs.command }} + run: mold -run cargo run ${{ steps.build_command.outputs.command }} - name: ❗ Warn on build failure if: ${{ failure() }} diff --git a/.github/workflows/deploy-master.yml b/.github/workflows/deploy-master.yml index 852af0ad..bf6c280b 100644 --- a/.github/workflows/deploy-master.yml +++ b/.github/workflows/deploy-master.yml @@ -31,7 +31,7 @@ jobs: - name: 🟢 Install the latest Node.js uses: actions/setup-node@v4 with: - node-version: 'latest' + node-version: "latest" - name: 🚧 Install build dependencies run: | @@ -49,9 +49,7 @@ jobs: - name: 🌐 Build Graphite web code env: NODE_ENV: production - run: | - cd frontend - mold -run npm run build + run: mold -run cargo run build web - name: 📤 Publish to Cloudflare Pages id: cloudflare diff --git a/.github/workflows/provide-shaders.yml b/.github/workflows/provide-shaders.yml index 7e38ecf3..b6006a7b 100644 --- a/.github/workflows/provide-shaders.yml +++ b/.github/workflows/provide-shaders.yml @@ -17,7 +17,7 @@ jobs: - uses: DeterminateSystems/magic-nix-cache-action@main - name: Build graphene raster nodes shaders - run: nix build .nix#graphite-raster-nodes-shaders && cp result raster_nodes_shaders_entrypoint.wgsl + run: nix build .#graphite-raster-nodes-shaders && cp result raster_nodes_shaders_entrypoint.wgsl - name: Upload graphene raster nodes shaders to artifacts repository run: | diff --git a/.nix/default.nix b/.nix/default.nix new file mode 100644 index 00000000..e77cc4a6 --- /dev/null +++ b/.nix/default.nix @@ -0,0 +1,81 @@ +inputs: + +let + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + forAllSystems = f: inputs.nixpkgs.lib.genAttrs systems (system: f system); + args = + system: + ( + let + lib = inputs.nixpkgs.lib // { + call = p: import p args; + }; + + pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ (import inputs.rust-overlay) ]; + }; + + info = { + pname = "graphite"; + version = "unstable"; + src = inputs.nixpkgs.lib.cleanSourceWith { + src = ./..; + filter = path: type: !(type == "directory" && builtins.baseNameOf path == ".nix"); + }; + cargoVendored = deps.crane.lib.vendorCargoDeps { inherit (info) src; }; + }; + + deps = { + crane = lib.call ./deps/crane.nix; + cef = lib.call ./deps/cef.nix; + rustGPU = lib.call ./deps/rust-gpu.nix; + }; + + args = { + inherit system; + inherit (inputs) self; + inherit inputs; + inherit pkgs; + inherit lib; + inherit info; + inherit deps; + } + // inputs; + in + args + ); + withArgs = f: forAllSystems (system: f (args system)); +in +{ + packages = withArgs ( + { lib, ... }: + rec { + default = graphite; + graphite = (lib.call ./pkgs/graphite.nix) { }; + graphite-dev = (lib.call ./pkgs/graphite.nix) { dev = true; }; + graphite-raster-nodes-shaders = lib.call ./pkgs/graphite-raster-nodes-shaders.nix; + graphite-branding = lib.call ./pkgs/graphite-branding.nix; + graphite-bundle = lib.call ./pkgs/graphite-bundle.nix; + graphite-flatpak-manifest = lib.call ./pkgs/graphite-flatpak-manifest.nix; + + # TODO: graphene-cli = lib.call ./pkgs/graphene-cli.nix; + + tools = { + third-party-licenses = lib.call ./pkgs/tools/third-party-licenses.nix; + }; + } + ); + + devShells = withArgs ( + { lib, ... }: + { + default = lib.call ./dev.nix; + } + ); + + formatter = withArgs ({ pkgs, ... }: pkgs.nixfmt-tree); +} diff --git a/.nix/flake.nix b/.nix/flake.nix deleted file mode 100644 index 270bb870..00000000 --- a/.nix/flake.nix +++ /dev/null @@ -1,110 +0,0 @@ -# This is a helper file for people using NixOS as their operating system. -# If you don't know what this file does, you can safely ignore it. -# This file defines the reproducible development environment for the project. -# -# Development Environment: -# - Provides all necessary tools for Rust/Wasm development -# - Includes dependencies for desktop app development -# - Sets up profiling and debugging tools -# - Configures mold as the default linker for faster builds -# -# Usage: -# - Development shell: `nix develop .nix` from the project root -# - Run in dev shell with direnv: add `use flake` to .envrc -{ - inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - crane.url = "github:ipetkov/crane"; - - # This is used to provide a identical development shell at `shell.nix` for users that do not use flakes - flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; - }; - - outputs = - inputs: - ( - let - systems = [ - "x86_64-linux" - "aarch64-linux" - ]; - forAllSystems = f: inputs.nixpkgs.lib.genAttrs systems (system: f system); - args = - system: - ( - let - lib = inputs.nixpkgs.lib // { - call = p: import p args; - }; - - pkgs = import inputs.nixpkgs { - inherit system; - overlays = [ (import inputs.rust-overlay) ]; - }; - - info = { - pname = "graphite"; - version = "unstable"; - src = inputs.nixpkgs.lib.cleanSourceWith { - src = ./..; - filter = path: type: !(type == "directory" && builtins.baseNameOf path == ".nix"); - }; - cargoVendored = deps.crane.lib.vendorCargoDeps { inherit (info) src; }; - }; - - deps = { - crane = lib.call ./deps/crane.nix; - cef = lib.call ./deps/cef.nix; - rustGPU = lib.call ./deps/rust-gpu.nix; - }; - - args = { - inherit system; - inherit (inputs) self; - inherit inputs; - inherit pkgs; - inherit lib; - inherit info; - inherit deps; - } - // inputs; - in - args - ); - withArgs = f: forAllSystems (system: f (args system)); - in - { - packages = withArgs ( - { lib, ... }: - rec { - default = graphite; - graphite = (lib.call ./pkgs/graphite.nix) { }; - graphite-dev = (lib.call ./pkgs/graphite.nix) { dev = true; }; - graphite-raster-nodes-shaders = lib.call ./pkgs/graphite-raster-nodes-shaders.nix; - graphite-branding = lib.call ./pkgs/graphite-branding.nix; - graphite-bundle = lib.call ./pkgs/graphite-bundle.nix; - graphite-flatpak-manifest = lib.call ./pkgs/graphite-flatpak-manifest.nix; - - # TODO: graphene-cli = lib.call ./pkgs/graphene-cli.nix; - - tools = { - third-party-licenses = lib.call ./pkgs/tools/third-party-licenses.nix; - }; - } - ); - - devShells = withArgs ( - { lib, ... }: - { - default = lib.call ./dev.nix; - } - ); - - formatter = withArgs ({ pkgs, ... }: pkgs.nixfmt-tree); - } - ); -} diff --git a/.nix/shell.nix b/.nix/shell.nix deleted file mode 100644 index 5b13af2e..00000000 --- a/.nix/shell.nix +++ /dev/null @@ -1,28 +0,0 @@ -# This is a helper file for people using NixOS as their operating system. -# If you don't know what this file does, you can safely ignore it. - -# If you are using Nix as your package manager, you can run 'nix-shell .nix' -# in the root directory of the project and Nix will open a bash shell -# with all the packages needed to build and run Graphite installed. -# A shell.nix file is used in the Nix ecosystem to define a development -# environment with specific dependencies. When you enter a Nix shell using -# this file, it ensures that all the specified tools and libraries are -# available regardless of the host system's configuration. This provides -# a reproducible development environment across different machines and developers. - -# You can enter the Nix shell and run Graphite like normal with: -# > npm start -# Or you can run it like this without needing to first enter the Nix shell: -# > nix-shell .nix --command "npm start" - -# Uses flake compat to provide a development shell that is identical to the one defined in the flake -(import ( - let - lock = builtins.fromJSON (builtins.readFile ./flake.lock); - nodeName = lock.nodes.root.inputs.flake-compat; - in - fetchTarball { - url = lock.nodes.${nodeName}.locked.url; - sha256 = lock.nodes.${nodeName}.locked.narHash; - } -) { src = ./.; }).shellNix diff --git a/Cargo.lock b/Cargo.lock index 6bf8977a..2fd2e3d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -731,6 +731,13 @@ dependencies = [ "serde", ] +[[package]] +name = "cargo-run" +version = "0.0.0" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "cargo-util-schemas" version = "0.8.2" @@ -6250,6 +6257,7 @@ dependencies = [ "scraper", "serde", "serde_json", + "thiserror 2.0.18", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 012978fb..46a53879 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "node-graph/node-macro", "node-graph/preprocessor", "proc-macros", + "tools/cargo-run", "tools/crate-hierarchy-viz", "tools/third-party-licenses", "tools/editor-message-tree", @@ -35,12 +36,12 @@ default-members = [ "libraries/path-bool", "libraries/math-parser", "node-graph/graph-craft", - "node-graph/graphene-cli", "node-graph/interpreted-executor", "node-graph/node-macro", "node-graph/preprocessor", # blocked by https://github.com/rust-lang/cargo/issues/16000 # "proc-macros", + "tools/cargo-run", ] resolver = "2" @@ -236,10 +237,6 @@ node-macro = { opt-level = 2 } lto = "thin" debug = true -[profile.profiling] -inherits = "release" -debug = true - [patch.crates-io] # Force cargo to use only one version of the dpi crate (vendoring breaks without this) dpi = { git = "https://github.com/rust-windowing/winit.git" } diff --git a/desktop/bundle/src/common.rs b/desktop/bundle/src/common.rs index 3e0523b8..88e16ff2 100644 --- a/desktop/bundle/src/common.rs +++ b/desktop/bundle/src/common.rs @@ -1,3 +1,5 @@ +#![cfg_attr(target_os = "linux", allow(unused))] // TODO: Remove this when bundling for linux is implemented + use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; @@ -27,8 +29,7 @@ pub(crate) fn cef_path() -> PathBuf { } pub(crate) fn build_bin(package: &str, bin: Option<&str>) -> Result> { - let profile = &profile_name(); - let mut args = vec!["build", "--package", package, "--profile", profile]; + let mut args = vec!["build", "--package", package, "--profile", profile_name()]; if let Some(bin) = bin { args.push("--bin"); args.push(bin); @@ -45,7 +46,7 @@ pub(crate) fn build_bin(package: &str, bin: Option<&str>) -> Result Result<(), Box> { let status = Command::new(program).args(args).stdout(Stdio::inherit()).stderr(Stdio::inherit()).status()?; if !status.success() { - std::process::exit(1); + return Err(format!("Command '{}' with args {:?} failed with status: {}", program, args, status).into()); } Ok(()) } diff --git a/desktop/bundle/src/linux.rs b/desktop/bundle/src/linux.rs index 94cd480e..f1511d8b 100644 --- a/desktop/bundle/src/linux.rs +++ b/desktop/bundle/src/linux.rs @@ -1,20 +1,19 @@ -use std::error::Error; - use crate::common::*; -pub fn main() -> Result<(), Box> { +pub fn main() -> Result<(), Box> { let app_bin = build_bin("graphite-desktop-platform-linux", None)?; // TODO: Implement bundling for linux // TODO: Consider adding more useful cli - if std::env::args().any(|a| a == "open") { - run_command(&app_bin.to_string_lossy(), &[]).expect("failed to open app"); + let args: Vec = std::env::args().collect(); + if let Some(pos) = args.iter().position(|a| a == "open") { + let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect(); + run_command(&app_bin.to_string_lossy(), &extra_args).expect("failed to open app"); } else { - println!("Binary built and placed at {}", app_bin.to_string_lossy()); + eprintln!("Binary built and placed at {}", app_bin.to_string_lossy()); eprintln!("Bundling for Linux is not yet implemented."); eprintln!("You can still start the app with the `open` subcommand. `cargo run -p graphite-desktop-bundle -- open`"); - std::process::exit(1); } Ok(()) diff --git a/desktop/bundle/src/mac.rs b/desktop/bundle/src/mac.rs index a3b74d01..3e53195e 100644 --- a/desktop/bundle/src/mac.rs +++ b/desktop/bundle/src/mac.rs @@ -22,9 +22,11 @@ pub fn main() -> Result<(), Box> { let app_dir = bundle(&profile_path, &app_bin, &helper_bin); // TODO: Consider adding more useful cli - if std::env::args().any(|a| a == "open") { - let executable_path = app_dir.join(EXEC_PATH).join(APP_NAME); - run_command(&executable_path.to_string_lossy(), &[]).expect("failed to open app"); + let args: Vec = std::env::args().collect(); + if let Some(pos) = args.iter().position(|a| a == "open") { + let executable = app_dir.join(EXEC_PATH).join(APP_NAME); + let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect(); + run_command(&executable.to_string_lossy(), &extra_args).expect("failed to open app"); } Ok(()) diff --git a/desktop/bundle/src/win.rs b/desktop/bundle/src/win.rs index 1b7a7279..6fc46fdd 100644 --- a/desktop/bundle/src/win.rs +++ b/desktop/bundle/src/win.rs @@ -12,9 +12,10 @@ pub fn main() -> Result<(), Box> { let executable = bundle(&profile_path(), &app_bin); // TODO: Consider adding more useful cli - if std::env::args().any(|a| a == "open") { - let executable_path = executable.to_string_lossy(); - run_command(&executable_path, &[]).expect("failed to open app") + let args: Vec = std::env::args().collect(); + if let Some(pos) = args.iter().position(|a| a == "open") { + let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect(); + run_command(&executable.to_string_lossy(), &extra_args).expect("failed to open app") } Ok(()) diff --git a/desktop/src/cef/context/builder.rs b/desktop/src/cef/context/builder.rs index e6ad3186..c47fae6c 100644 --- a/desktop/src/cef/context/builder.rs +++ b/desktop/src/cef/context/builder.rs @@ -1,11 +1,10 @@ -use std::path::{Path, PathBuf}; - use cef::args::Args; -use cef::sys::{CEF_API_VERSION_LAST, cef_log_severity_t, cef_resultcode_t}; +use cef::sys::{CEF_API_VERSION_LAST, cef_log_severity_t}; use cef::{ App, BrowserSettings, CefString, Client, DictionaryValue, ImplCommandLine, ImplRequestContext, LogSeverity, RequestContextSettings, SchemeHandlerFactory, Settings, WindowInfo, api_hash, browser_host_create_browser_sync, execute_process, }; +use std::path::{Path, PathBuf}; use super::CefContext; use super::singlethreaded::SingleThreadedCefContext; @@ -138,8 +137,7 @@ impl CefContextBuilder { }); } Err(e) => { - tracing::error!("Failed to initialize CEF context: {:?}", e); - std::process::exit(1); + panic!("Failed to initialize CEF context: {:?}", e); } }); @@ -153,10 +151,7 @@ impl CefContextBuilder { let result = cef::initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut()); if result != 1 { let cef_exit_code = cef::get_exit_code() as u32; - if cef_exit_code == cef_resultcode_t::CEF_RESULT_CODE_NORMAL_EXIT_PROCESS_NOTIFIED as u32 { - return Err(InitError::AlreadyRunning); - } - return Err(InitError::InitializationFailed(cef_exit_code)); + return Err(InitError::InitializationFailureCode(cef_exit_code)); } Ok(()) } @@ -228,18 +223,16 @@ fn create_browser(event_handler: H, instance_dir: PathBuf, d pub(crate) enum SetupError { #[error("This is the sub process should exit immediately")] Subprocess, - #[error("Subprocess returned non zero exit code")] + #[error("Subprocess returned non zero exit code: {0}")] SubprocessFailed(String), } #[derive(thiserror::Error, Debug)] pub(crate) enum InitError { - #[error("Initialization failed")] - InitializationFailed(u32), + #[error("Initialization failed with code: {0}")] + InitializationFailureCode(u32), #[error("Browser creation failed")] BrowserCreationFailed, #[error("Request context creation failed")] RequestContextCreationFailed, - #[error("Another instance is already running")] - AlreadyRunning, } diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index f56245e0..bc29f729 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -5,7 +5,6 @@ use crate::consts::APP_LOCK_FILE_NAME; use crate::event::CreateAppEventSchedulerEventLoopExt; use clap::Parser; use std::io::Write; -use std::process::exit; use tracing_subscriber::EnvFilter; use winit::event_loop::EventLoop; @@ -46,8 +45,7 @@ pub fn start() { .truncate(true) .open(dirs::app_data_dir().join(APP_LOCK_FILE_NAME)) else { - tracing::error!("Failed to open lock file, Exiting."); - exit(1); + panic!("Failed to open lock file.") }; let mut lock = fd_lock::RwLock::new(lock_file); let lock = match lock.try_write() { @@ -60,7 +58,7 @@ pub fn start() { } Err(_) => { tracing::error!("Another instance is already running, Exiting."); - exit(1); + std::process::exit(1); } }; @@ -86,21 +84,14 @@ pub fn start() { tracing::info!("CEF initialized successfully"); context } - Err(cef::InitError::AlreadyRunning) => { - tracing::error!("Another instance is already running, Exiting."); - exit(1); - } - Err(cef::InitError::InitializationFailed(code)) => { - tracing::error!("Cef initialization failed with code: {code}"); - exit(1); + Err(cef::InitError::InitializationFailureCode(code)) => { + panic!("CEF initialization failed with code: {code}"); } Err(cef::InitError::BrowserCreationFailed) => { - tracing::error!("Failed to create CEF browser"); - exit(1); + panic!("Failed to create CEF browser"); } Err(cef::InitError::RequestContextCreationFailed) => { - tracing::error!("Failed to create CEF request context"); - exit(1); + panic!("Failed to create CEF request context"); } }; @@ -139,7 +130,7 @@ pub fn start() { // Calling `exit` bypasses rust teardown and lets Windows perform process cleanup. // TODO: Identify and fix the underlying CEF shutdown issue so this workaround can be removed. #[cfg(target_os = "windows")] - exit(0); + std::process::exit(0); } pub fn start_helper() { diff --git a/.nix/flake.lock b/flake.lock similarity index 72% rename from .nix/flake.lock rename to flake.lock index 7bb7e2f2..fbc34b8a 100644 --- a/.nix/flake.lock +++ b/flake.lock @@ -15,20 +15,6 @@ "type": "github" } }, - "flake-compat": { - "locked": { - "lastModified": 1733328505, - "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", - "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", - "revCount": 69, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" - } - }, "nixpkgs": { "locked": { "lastModified": 1770197578, @@ -48,7 +34,6 @@ "root": { "inputs": { "crane": "crane", - "flake-compat": "flake-compat", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" } diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..04f0e80f --- /dev/null +++ b/flake.nix @@ -0,0 +1,25 @@ +# This is a helper file for people using NixOS as their operating system. +# If you don't know what this file does, you can safely ignore it. +# This file defines the reproducible development environment for the project. +# +# Development Environment: +# - Provides all necessary tools for Rust/Wasm development +# - Includes dependencies for desktop app development +# - Sets up profiling and debugging tools +# - Configures mold as the default linker for faster builds +# +# Usage: +# - Development shell: `nix develop` from the project root +# - Run in dev shell with direnv: add `use flake` to .envrc +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + crane.url = "github:ipetkov/crane"; + }; + + outputs = inputs: import ./.nix inputs; +} diff --git a/frontend/package.json b/frontend/package.json index ac5bc4fb..fc70d5f7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,12 +8,10 @@ "scripts": { "---------- DEV SERVER ----------": "", "start": "npm run setup && npm run wasm:build-dev && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-dev\"", - "profiling": "npm run setup && npm run wasm:build-profiling && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-profiling\"", "production": "npm run setup && npm run wasm:build-production && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-production\"", "---------- BUILDS ----------": "", "build": "npm run setup && npm run wasm:build-production && vite build", "build-dev": "npm run setup && npm run wasm:build-dev && vite build --mode dev", - "build-profiling": "npm run setup && npm run wasm:build-profiling && vite build --mode dev", "build-native": "npm run setup && npm run native:build-production", "build-native-dev": "npm run setup && npm run native:build-dev", "---------- UTILITIES ----------": "", @@ -24,10 +22,8 @@ "native:build-dev": "wasm-pack build ./wasm --dev --target=web --no-default-features --features native && vite build --mode native", "native:build-production": "wasm-pack build ./wasm --release --target=web --no-default-features --features native && vite build --mode native", "wasm:build-dev": "wasm-pack build ./wasm --dev --target=web", - "wasm:build-profiling": "wasm-pack build ./wasm --profiling --target=web", "wasm:build-production": "wasm-pack build ./wasm --release --target=web", "wasm:watch-dev": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --dev --target=web -- --color=always\"", - "wasm:watch-profiling": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --profiling --target=web -- --color=always\"", "wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --release --target=web -- --color=always\"" }, "//": "NOTE: `source-sans-pro` is never to be upgraded to 3.x because that renders a pixel above its intended position.", diff --git a/frontend/wasm/Cargo.toml b/frontend/wasm/Cargo.toml index a4d21762..c1632a0f 100644 --- a/frontend/wasm/Cargo.toml +++ b/frontend/wasm/Cargo.toml @@ -57,14 +57,6 @@ debug-js-glue = false demangle-name-section = false dwarf-debug-info = false -[package.metadata.wasm-pack.profile.profiling] -wasm-opt = ["-Os", "-g"] - -[package.metadata.wasm-pack.profile.profiling.wasm-bindgen] -debug-js-glue = true -demangle-name-section = true -dwarf-debug-info = true - [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(wasm_bindgen_unstable_test_coverage)', diff --git a/node-graph/graph-craft/.cargo/config.toml b/node-graph/graph-craft/.cargo/config.toml deleted file mode 100644 index a4ae00c0..00000000 --- a/node-graph/graph-craft/.cargo/config.toml +++ /dev/null @@ -1,3 +0,0 @@ -[profile.profiling] -inherits = "release" -debug = true diff --git a/node-graph/nodes/raster/shaders/build.rs b/node-graph/nodes/raster/shaders/build.rs index a6afe8f2..29901d2b 100644 --- a/node-graph/nodes/raster/shaders/build.rs +++ b/node-graph/nodes/raster/shaders/build.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; pub fn main() -> Result<(), Box> { env_logger::builder().filter_level(log::LevelFilter::Debug).init(); - // Skip building the shader if they are provided externally + // Skip building the shaders if they are provided externally println!("cargo:rerun-if-env-changed=RASTER_NODES_SHADER_PATH"); if !std::env::var("RASTER_NODES_SHADER_PATH").unwrap_or_default().is_empty() { return Ok(()); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index a8f72380..00000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Graphite", - "lockfileVersion": 2, - "requires": true, - "packages": {} -} diff --git a/package.json b/package.json deleted file mode 100644 index b49c6ca2..00000000 --- a/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "description": "A convenience package for calling the real package.json in ./frontend", - "private": true, - "scripts": { - "---------- DEV SERVER ----------": "", - "start": "cd frontend && npm start", - "start-desktop": "cd frontend && npm run build-native-dev && cargo run -p third-party-licenses --features desktop && cargo run -p graphite-desktop-bundle -- open", - "profiling": "cd frontend && npm run profiling", - "production": "cd frontend && npm run production", - "---------- BUILDS ----------": "", - "build-dev": "cd frontend && npm run build-dev", - "build-profiling": "cd frontend && npm run build-profiling", - "build": "cd frontend && npm run build", - "build-desktop": "cd frontend && npm run build-native && cargo run -p third-party-licenses --features desktop && cargo run -r -p graphite-desktop-bundle", - "build-desktop-dev": "cd frontend && npm run build-native-dev && cargo run -p third-party-licenses --features desktop && cargo run -p graphite-desktop-bundle", - "---------- UTILITIES ----------": "", - "lint": "cd frontend && npm run lint", - "lint-fix": "cd frontend && npm run lint-fix" - } -} diff --git a/tools/cargo-run/Cargo.toml b/tools/cargo-run/Cargo.toml new file mode 100644 index 00000000..95385fc4 --- /dev/null +++ b/tools/cargo-run/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cargo-run" +edition.workspace = true +version.workspace = true +license.workspace = true +authors.workspace = true + +default-run = "cargo-run" + +[dependencies] +thiserror = { workspace = true } diff --git a/tools/cargo-run/src/lib.rs b/tools/cargo-run/src/lib.rs new file mode 100644 index 00000000..46de74f6 --- /dev/null +++ b/tools/cargo-run/src/lib.rs @@ -0,0 +1,104 @@ +use std::path::PathBuf; +use std::process; + +pub mod requirements; + +pub enum Action { + Run, + Build, +} + +pub enum Target { + Web, + Desktop, + Cli, +} + +pub enum Profile { + Default, + Release, + Debug, +} + +pub struct Task { + pub action: Action, + pub target: Target, + pub profile: Profile, + pub args: Vec, +} + +impl Task { + pub fn parse(args: &[&str]) -> Option { + let split = args.iter().position(|a| *a == "--").unwrap_or(args.len()); + let passthru_args = args[split..].iter().skip(1).map(|s| s.to_string()).collect(); + let args = &args[..split]; + + let (action, args) = match args.first() { + Some(&"build") => (Action::Build, &args[1..]), + Some(&"run") => (Action::Run, &args[1..]), + Some(&"help") => return None, + _ => (Action::Run, args), + }; + + let (target, args) = match args.first() { + Some(&"desktop") => (Target::Desktop, &args[1..]), + Some(&"web") => (Target::Web, &args[1..]), + Some(&"cli") => (Target::Cli, &args[1..]), + _ => (Target::Web, args), + }; + + let profile = match args.first() { + Some(&"release") => Profile::Release, + Some(&"debug") => Profile::Debug, + None => Profile::Default, + _ => return None, + }; + + Some(Task { + target, + action, + profile, + args: passthru_args, + }) + } +} + +pub fn run(command: &str) -> Result<(), Error> { + run_from(command, None) +} + +pub fn npm_run_in_frontend_dir(args: &str) -> Result<(), Error> { + let workspace_dir = std::path::PathBuf::from(env!("CARGO_WORKSPACE_DIR")); + let frontend_dir = workspace_dir.join("frontend"); + let npm = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" }; + run_from(&format!("{npm} run {args}"), Some(&frontend_dir)) +} + +fn run_from(command: &str, dir: Option<&PathBuf>) -> Result<(), Error> { + let command = command.split_whitespace().collect::>(); + let mut cmd = process::Command::new(command[0]); + if command.len() > 1 { + cmd.args(&command[1..]); + } + if let Some(dir) = dir { + cmd.current_dir(dir); + } + let exit_code = cmd + .spawn() + .map_err(|e| Error::Io(e, format!("Failed to spawn command '{}'", command.join(" "))))? + .wait() + .map_err(|e| Error::Io(e, format!("Failed to wait for command '{}'", command.join(" "))))?; + if !exit_code.success() { + return Err(Error::Command(command.join(" "), exit_code)); + } + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{1}: {0}")] + Io(#[source] std::io::Error, String), + + #[error("Command '{0}' exited with code {1}")] + Command(String, process::ExitStatus), +} diff --git a/tools/cargo-run/src/main.rs b/tools/cargo-run/src/main.rs new file mode 100644 index 00000000..7527de06 --- /dev/null +++ b/tools/cargo-run/src/main.rs @@ -0,0 +1,99 @@ +use std::process::ExitCode; + +use cargo_run::*; + +fn usage() { + println!(); + println!("USAGE:"); + println!(" cargo run [] [] [] [-- [args]...]"); + println!(); + println!("COMMON USAGE:"); + println!(" cargo run Run the web app"); + println!(" cargo run desktop Run the desktop app"); + println!(); + println!("OPTIONS:"); + println!(":"); + println!(" [run] Run the selected target (default)"); + println!(" build Build the selected target"); + println!(" help Show this message"); + println!(":"); + println!(" [web] Web app (default)"); + println!(" desktop Desktop app"); + println!(" cli Graphene CLI"); + println!(":"); + println!(" [debug] Optimizations disabled (default for run)"); + println!(" [release] Optimizations enabled (default for build)"); + println!(); + println!("MORE EXAMPLES:"); + println!(" cargo run build desktop"); + println!(" cargo run desktop release"); + println!(" cargo run cli -- --help"); + println!() +} + +fn main() -> ExitCode { + let args: Vec = std::env::args().collect(); + let args: Vec<&str> = args.iter().skip(1).map(String::as_str).collect(); + + let task = match Task::parse(&args) { + Some(run) => run, + None => { + usage(); + return ExitCode::SUCCESS; + } + }; + + if let Err(e) = run_task(&task) { + eprintln!("Error: {e}"); + return ExitCode::FAILURE; + } + ExitCode::SUCCESS +} + +fn run_task(task: &Task) -> Result<(), Error> { + requirements::check(task)?; + + match (&task.action, &task.target, &task.profile) { + (Action::Run, Target::Web, Profile::Debug | Profile::Default) => npm_run_in_frontend_dir("start")?, + (Action::Run, Target::Web, Profile::Release) => npm_run_in_frontend_dir("production")?, + + (Action::Build, Target::Web, Profile::Debug) => npm_run_in_frontend_dir("build-dev")?, + (Action::Build, Target::Web, Profile::Release | Profile::Default) => npm_run_in_frontend_dir("build")?, + + (action, Target::Desktop, mut profile) => { + if matches!(profile, Profile::Default) { + profile = match action { + Action::Run => &Profile::Debug, + Action::Build => &Profile::Release, + } + } + + if matches!(profile, Profile::Release) { + npm_run_in_frontend_dir("build-native")?; + } else { + npm_run_in_frontend_dir("build-native-dev")?; + }; + + run("cargo run -p third-party-licenses --features desktop")?; + + let cargo_profile = match profile { + Profile::Debug => "dev", + Profile::Release => "release", + Profile::Default => unreachable!(), + }; + let args = if matches!(action, Action::Run) { + format!(" -- open {}", task.args.join(" ")) + } else { + "".to_string() + }; + run(&format!("cargo run --profile {cargo_profile} -p graphite-desktop-bundle{args}"))?; + } + + (Action::Run, Target::Cli, Profile::Debug | Profile::Default) => run(&format!("cargo run -p graphene-cli -- {}", task.args.join(" ")))?, + (Action::Run, Target::Cli, Profile::Release) => run(&format!("cargo run -r -p graphene-cli -- {}", task.args.join(" ")))?, + + (Action::Build, Target::Cli, Profile::Debug) => run("cargo build -p graphene-cli")?, + (Action::Build, Target::Cli, Profile::Release | Profile::Default) => run("cargo build -r -p graphene-cli")?, + } + Ok(()) +} diff --git a/tools/cargo-run/src/requirements.rs b/tools/cargo-run/src/requirements.rs new file mode 100644 index 00000000..9da2a0e7 --- /dev/null +++ b/tools/cargo-run/src/requirements.rs @@ -0,0 +1,196 @@ +use std::io::IsTerminal; +use std::process::Command; + +use crate::*; + +#[derive(Default, Clone)] +struct Requirement { + command: &'static str, + args: &'static [&'static str], + name: &'static str, + version: Option<&'static str>, + install: Option<&'static str>, + skip: Option<&'static dyn Fn(&Task) -> bool>, +} + +fn requirements(task: &Task) -> Vec { + [ + Requirement { + command: "rustc", + args: &["--version"], + name: "Rust", + ..Default::default() + }, + Requirement { + command: "cargo-about", + args: &["--version"], + name: "cargo-about", + install: Some("cargo install cargo-about"), + skip: Some(&|task| matches!(task.target, Target::Cli)), + ..Default::default() + }, + Requirement { + command: "cargo-watch", + args: &["--version"], + name: "cargo-watch", + install: Some("cargo install cargo-watch"), + skip: Some(&|task| { + !matches!( + task, + Task { + target: Target::Web, + action: Action::Run, + .. + } + ) + }), + ..Default::default() + }, + Requirement { + command: "wasm-bindgen", + args: &["--version"], + name: "wasm-bindgen-cli", + version: Some("0.2.100"), + install: Some("cargo install -f wasm-bindgen-cli@0.2.100"), + skip: Some(&|task| matches!(task.target, Target::Cli)), + }, + Requirement { + command: "wasm-pack", + args: &["--version"], + name: "wasm-pack", + install: Some("cargo install wasm-pack"), + skip: Some(&|task| matches!(task.target, Target::Cli)), + ..Default::default() + }, + Requirement { + command: "node", + args: &["--version"], + name: "Node.js", + skip: Some(&|task| matches!(task.target, Target::Cli)), + ..Default::default() + }, + Requirement { + command: "cmake", + args: &["--version"], + name: "CMake", + skip: Some(&|task| !matches!(task.target, Target::Desktop) || cfg!(target_os = "linux")), + ..Default::default() + }, + Requirement { + command: "ninja", + args: &["--version"], + name: "Ninja", + skip: Some(&|task| !matches!(task.target, Target::Desktop) || cfg!(target_os = "linux")), + ..Default::default() + }, + ] + .iter() + .filter(|d| if let Some(skip) = d.skip { !skip(task) } else { true }) + .cloned() + .collect() +} + +pub fn check(task: &Task) -> Result<(), Error> { + eprintln!(); + eprintln!("Checking Requirements:"); + + let mut installable: Vec = Vec::new(); + let mut failures: Vec = Vec::new(); + + for dep in requirements(task) { + match Command::new(dep.command).args(dep.args).output() { + Ok(output) if output.status.success() => { + let version = String::from_utf8_lossy(&output.stdout); + let version = version.lines().next().unwrap_or_default().trim(); + + if let Some(expected) = dep.version { + if version.contains(expected) { + eprintln!(" ✓ {} ({})", dep.name, version); + } else { + eprintln!(" ✗ {} (found {}, expected {})", dep.name, version, expected); + if dep.install.is_some() { + installable.push(dep); + } else { + failures.push(format!("{}: version mismatch (found {version}, expected {expected})", dep.name)); + } + } + } else { + eprintln!(" ✓ {} ({})", dep.name, version); + } + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!(" ✗ {} - command failed: {}", dep.name, stderr.trim()); + if dep.install.is_some() { + installable.push(dep); + } else { + failures.push(format!("{}: not installed or not working", dep.name)); + } + } + Err(_) => { + eprintln!(" ✗ {} - not found", dep.name); + if dep.install.is_some() { + installable.push(dep); + } else { + failures.push(format!("{}: not found in PATH", dep.name)); + } + } + } + } + + eprintln!(); + + if installable.is_empty() && failures.is_empty() { + return Ok(()); + } + + let total = installable.len() + failures.len(); + eprintln!("{total} requirement{} not met:", if total > 1 { "s" } else { "" }); + for dep in &installable { + eprintln!(" - {}: {}", dep.name, dep.install.unwrap()); + } + for msg in &failures { + eprintln!(" - {msg}"); + } + + if !failures.is_empty() { + eprintln!(); + eprintln!("See: https://graphite.art/volunteer/guide/project-setup/"); + } + + // Don't prompt for automatic installation if we're not interactive session + if !std::io::stdout().is_terminal() || !std::io::stderr().is_terminal() || !std::io::stdin().is_terminal() { + return Ok(()); + } + + if installable.is_empty() { + return Ok(()); + } + + eprintln!(); + eprintln!("The following can be installed automatically:"); + for dep in &installable { + eprintln!(" {}", dep.install.unwrap()); + } + eprintln!(); + eprint!("Install them now? [Y/n] "); + + let mut input = String::new(); + std::io::stdin().read_line(&mut input).map_err(|e| Error::Io(e, "Failed to read from stdin".into()))?; + let input = input.trim(); + + if input.is_empty() || input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes") { + for dep in &installable { + let parts: Vec<&str> = dep.install.unwrap().split_whitespace().collect(); + eprintln!("Running: {}...", dep.install.unwrap()); + let status = Command::new(parts[0]) + .args(&parts[1..]) + .status() + .map_err(|e| Error::Io(e, format!("Failed to run '{}'", dep.install.unwrap())))?; + if !status.success() { + eprintln!("Failed to install {}", dep.name); + } + } + } + Ok(()) +} diff --git a/tools/third-party-licenses/Cargo.toml b/tools/third-party-licenses/Cargo.toml index 4a599673..fe348963 100644 --- a/tools/third-party-licenses/Cargo.toml +++ b/tools/third-party-licenses/Cargo.toml @@ -13,6 +13,7 @@ desktop = ["dep:cef-dll-sys", "dep:scraper"] serde = { workspace = true } serde_json = { workspace = true } lzma-rust2 = { workspace = true } +thiserror = { workspace = true } # Optional workspace dependencies cef-dll-sys = { workspace = true, optional = true } diff --git a/tools/third-party-licenses/src/cargo.rs b/tools/third-party-licenses/src/cargo.rs index 49d6731d..7b10a4fa 100644 --- a/tools/third-party-licenses/src/cargo.rs +++ b/tools/third-party-licenses/src/cargo.rs @@ -1,9 +1,9 @@ -use crate::{LicenceSource, LicenseEntry, Package}; +use crate::{Error, LicenceSource, LicenseEntry, Package}; use serde::Deserialize; use std::fs; use std::hash::{Hash, Hasher}; use std::path::PathBuf; -use std::process::{self, Command}; +use std::process::Command; pub struct CargoLicenseSource {} @@ -14,8 +14,8 @@ impl CargoLicenseSource { } impl LicenceSource for CargoLicenseSource { - fn licenses(&self) -> Vec { - parse(run()) + fn licenses(&self) -> Result, Error> { + Ok(parse(run()?)) } } @@ -84,23 +84,18 @@ fn parse(parsed: Output) -> Vec { .collect() } -fn run() -> Output { +fn run() -> Result { let output = Command::new("cargo") .args(["about", "generate", "--format", "json", "--frozen"]) .current_dir(env!("CARGO_WORKSPACE_DIR")) .output() - .unwrap_or_else(|e| { - eprintln!("Failed to run cargo about generate: {e}"); - process::exit(1) - }); + .map_err(|e| Error::Io(e, "Failed to run cargo about generate".into()))?; if !output.status.success() { - eprintln!("cargo about generate failed:\n{}", String::from_utf8_lossy(&output.stderr)); - process::exit(1) + return Err(Error::Command(format!("cargo about generate failed:\n{}", String::from_utf8_lossy(&output.stderr)))); } - serde_json::from_str(&String::from_utf8(output.stdout).expect("cargo about generate should return valid UTF-8")).unwrap_or_else(|e| { - eprintln!("Failed to parse cargo about generate JSON: {e}"); - process::exit(1) - }) + let stdout = String::from_utf8(output.stdout).map_err(|e| Error::Utf8(e, "cargo about generate returned invalid UTF-8".into()))?; + + serde_json::from_str(&stdout).map_err(|e| Error::Json(e, "Failed to parse cargo about generate JSON".into())) } diff --git a/tools/third-party-licenses/src/cef.rs b/tools/third-party-licenses/src/cef.rs index 94fe45d0..b2eaa827 100644 --- a/tools/third-party-licenses/src/cef.rs +++ b/tools/third-party-licenses/src/cef.rs @@ -1,11 +1,11 @@ use lzma_rust2::XzReader; use scraper::{Html, Selector}; +use std::fs; use std::hash::Hash; use std::io::Read; use std::path::PathBuf; -use std::{fs, process}; -use crate::{LicenceSource, LicenseEntry, Package}; +use crate::{Error, LicenceSource, LicenseEntry, Package}; pub struct CefLicenseSource; @@ -16,15 +16,15 @@ impl CefLicenseSource { } impl LicenceSource for CefLicenseSource { - fn licenses(&self) -> Vec { - let html = read(); - parse(&html) + fn licenses(&self) -> Result, Error> { + let html = read()?; + Ok(parse(&html)) } } impl Hash for CefLicenseSource { fn hash(&self, state: &mut H) { - read().hash(state) + read().unwrap().hash(state) } } @@ -64,42 +64,29 @@ fn parse(html: &str) -> Vec { .collect() } -fn read() -> String { +fn read() -> Result { let cef_path = PathBuf::from(env!("CEF_PATH")); let cef_credits = std::fs::read_dir(&cef_path) - .unwrap_or_else(|e| { - eprintln!("Failed to read CEF_PATH directory {}: {e}", cef_path.display()); - process::exit(1); - }) + .map_err(|e| Error::Io(e, format!("Failed to read CEF_PATH directory {}", cef_path.display())))? .filter_map(|entry| entry.ok()) .find(|entry| { let name = entry.file_name(); name.eq_ignore_ascii_case("credits.html") || name.eq_ignore_ascii_case("credits.html.xz") }) .map(|entry| entry.path()) - .unwrap_or_else(|| { - eprintln!("Could not find CREDITS.html or CREDITS.html.xz in {}", cef_path.display()); - process::exit(1); - }); + .ok_or_else(|| Error::CefCreditsNotFound(cef_path.clone()))?; let decompress_xz = cef_credits.extension().map(|ext| ext.eq_ignore_ascii_case("xz")).unwrap_or(false); if decompress_xz { - let file = fs::File::open(&cef_credits).unwrap_or_else(|e| { - eprintln!("Failed to open CEF credits file {}: {e}", cef_credits.display()); - process::exit(1); - }); + let file = fs::File::open(&cef_credits).map_err(|e| Error::Io(e, format!("Failed to open CEF credits file {}", cef_credits.display())))?; let mut reader = XzReader::new(file, false); let mut html = String::new(); - reader.read_to_string(&mut html).unwrap_or_else(|e| { - eprintln!("Failed to decompress CEF credits file {}: {e}", cef_credits.display()); - process::exit(1); - }); - html + reader + .read_to_string(&mut html) + .map_err(|e| Error::Io(e, format!("Failed to decompress CEF credits file {}", cef_credits.display())))?; + Ok(html) } else { - fs::read_to_string(&cef_credits).unwrap_or_else(|e| { - eprintln!("Failed to read CEF credits file {}: {e}", cef_credits.display()); - process::exit(1); - }) + fs::read_to_string(&cef_credits).map_err(|e| Error::Io(e, format!("Failed to read CEF credits file {}", cef_credits.display()))) } } diff --git a/tools/third-party-licenses/src/main.rs b/tools/third-party-licenses/src/main.rs index afa1b501..797411ad 100644 --- a/tools/third-party-licenses/src/main.rs +++ b/tools/third-party-licenses/src/main.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; +use std::fs; use std::hash::{DefaultHasher, Hash, Hasher}; use std::path::PathBuf; -use std::{fs, process}; +use std::process::ExitCode; mod cargo; #[cfg(feature = "desktop")] @@ -13,8 +14,27 @@ use crate::cargo::CargoLicenseSource; use crate::cef::CefLicenseSource; use crate::npm::NpmLicenseSource; +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{1}: {0}")] + Io(#[source] std::io::Error, String), + + #[error("{1}: {0}")] + Json(#[source] serde_json::Error, String), + + #[error("{1}: {0}")] + Utf8(#[source] std::string::FromUtf8Error, String), + + #[error("{0}")] + Command(String), + + #[cfg(feature = "desktop")] + #[error("Could not find CREDITS.html or CREDITS.html.xz in {0}")] + CefCreditsNotFound(PathBuf), +} + pub trait LicenceSource: std::hash::Hash { - fn licenses(&self) -> Vec; + fn licenses(&self) -> Result, Error>; } pub struct LicenseEntry { @@ -38,7 +58,15 @@ struct Run<'a> { cef: &'a CefLicenseSource, } -fn main() { +fn main() -> ExitCode { + if let Err(e) = run() { + eprintln!("Error: {e}"); + return ExitCode::FAILURE; + } + ExitCode::SUCCESS +} + +fn run() -> Result<(), Error> { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let workspace_dir = PathBuf::from(env!("CARGO_WORKSPACE_DIR")); @@ -71,32 +99,26 @@ fn main() { if current_hash == fs::read_to_string(¤t_hash_path).unwrap_or_default() { eprintln!("No changes in licenses detected, skipping generation."); - return; + return Ok(()); } eprintln!("Changes in licenses detected, generating new license file."); let licenses = merge_filter_dedup_and_sort(vec![ - cargo_source.licenses(), - npm_source.licenses(), + cargo_source.licenses()?, + npm_source.licenses()?, #[cfg(feature = "desktop")] - cef_source.licenses(), + cef_source.licenses()?, ]); let formatted = format_credits(&licenses); #[cfg(feature = "desktop")] - let output = compress(&formatted); + let output = compress(&formatted)?; #[cfg(not(feature = "desktop"))] let output = formatted.as_bytes().to_vec(); if let Some(parent) = output_path.parent() { - fs::create_dir_all(parent).unwrap_or_else(|e| { - eprintln!("Failed to create directory {}: {e}", parent.display()); - std::process::exit(1); - }); + fs::create_dir_all(parent).map_err(|e| Error::Io(e, format!("Failed to create directory {}", parent.display())))?; } - fs::write(&output_path, &output).unwrap_or_else(|e| { - eprintln!("Failed to write {}: {e}", &output_path.display()); - std::process::exit(1); - }); + fs::write(&output_path, &output).map_err(|e| Error::Io(e, format!("Failed to write {}", output_path.display())))?; run.output = &output; let hash = { @@ -105,10 +127,9 @@ fn main() { format!("{:016x}", hasher.finish()) }; - fs::write(¤t_hash_path, hash).unwrap_or_else(|e| { - eprintln!("Failed to write hash file {}: {e}", current_hash_path.display()); - process::exit(1); - }); + fs::write(¤t_hash_path, hash).map_err(|e| Error::Io(e, format!("Failed to write hash file {}", current_hash_path.display())))?; + + Ok(()) } fn format_credits(licenses: &Vec) -> String { @@ -210,20 +231,11 @@ fn dedup_by_licence_text(vec: Vec) -> Vec { } #[cfg(feature = "desktop")] -fn compress(content: &str) -> Vec { +fn compress(content: &str) -> Result, Error> { use std::io::Write; let mut buf = Vec::new(); - let mut writer = lzma_rust2::XzWriter::new(&mut buf, lzma_rust2::XzOptions::default()).unwrap_or_else(|e| { - eprintln!("Failed to create XZ writer: {e}"); - std::process::exit(1); - }); - writer.write_all(content.as_bytes()).unwrap_or_else(|e| { - eprintln!("Failed to write compressed credits: {e}"); - std::process::exit(1); - }); - writer.finish().unwrap_or_else(|e| { - eprintln!("Failed to finish XZ compression: {e}"); - std::process::exit(1); - }); - buf + let mut writer = lzma_rust2::XzWriter::new(&mut buf, lzma_rust2::XzOptions::default()).map_err(|e| Error::Io(e, "Failed to create XZ writer".into()))?; + writer.write_all(content.as_bytes()).map_err(|e| Error::Io(e, "Failed to write compressed credits".into()))?; + writer.finish().map_err(|e| Error::Io(e, "Failed to finish XZ compression".into()))?; + Ok(buf) } diff --git a/tools/third-party-licenses/src/npm.rs b/tools/third-party-licenses/src/npm.rs index aac94f5c..01db5003 100644 --- a/tools/third-party-licenses/src/npm.rs +++ b/tools/third-party-licenses/src/npm.rs @@ -1,10 +1,9 @@ use std::collections::HashMap; use std::fs; use std::path::PathBuf; -use std::process; use std::process::Command; -use crate::{LicenceSource, LicenseEntry, Package}; +use crate::{Error, LicenceSource, LicenseEntry, Package}; pub struct NpmLicenseSource { dir: PathBuf, @@ -16,8 +15,8 @@ impl NpmLicenseSource { } impl LicenceSource for NpmLicenseSource { - fn licenses(&self) -> Vec { - parse(run(&self.dir)) + fn licenses(&self) -> Result, Error> { + Ok(parse(run(&self.dir)?)) } } @@ -66,7 +65,7 @@ fn parse(parsed: Output) -> Vec { .collect() } -fn run(dir: &std::path::Path) -> Output { +fn run(dir: &std::path::Path) -> Result { #[cfg(not(target_os = "windows"))] let mut cmd = Command::new("npx"); #[cfg(target_os = "windows")] @@ -74,20 +73,13 @@ fn run(dir: &std::path::Path) -> Output { cmd.args(["license-checker-rseidelsohn", "--production", "--json"]); cmd.current_dir(dir); - let output = cmd.output().unwrap_or_else(|e| { - eprintln!("Failed to run npx license-checker-rseidelsohn: {e}"); - process::exit(1); - }); + let output = cmd.output().map_err(|e| Error::Io(e, "Failed to run npx license-checker-rseidelsohn".into()))?; if !output.status.success() { - eprintln!("npx license-checker-rseidelsohn failed:\n{}", String::from_utf8_lossy(&output.stderr)); - process::exit(1); + return Err(Error::Command(format!("npx license-checker-rseidelsohn failed:\n{}", String::from_utf8_lossy(&output.stderr)))); } - let json_str = String::from_utf8(output.stdout).expect("Invalid UTF-8 from license-checker"); + let json_str = String::from_utf8(output.stdout).map_err(|e| Error::Utf8(e, "Invalid UTF-8 from license-checker".into()))?; - serde_json::from_str(&json_str).unwrap_or_else(|e| { - eprintln!("Failed to parse license-checker JSON: {e}"); - process::exit(1) - }) + serde_json::from_str(&json_str).map_err(|e| Error::Json(e, "Failed to parse license-checker JSON".into())) } diff --git a/website/content/volunteer/guide/project-setup/_index.md b/website/content/volunteer/guide/project-setup/_index.md index 3934df3a..6c16070a 100644 --- a/website/content/volunteer/guide/project-setup/_index.md +++ b/website/content/volunteer/guide/project-setup/_index.md @@ -12,21 +12,10 @@ To begin working with the Graphite codebase, you will need to set up the project ## Dependencies Graphite is built with Rust and web technologies, which means you will need to install: -- [Node.js](https://nodejs.org/) (the latest LTS version) - [Rust](https://www.rust-lang.org/) (the latest stable release) +- [Node.js](https://nodejs.org/) (the latest LTS version) - [Git](https://git-scm.com/) (any recent version) -Next, install the dependencies required for development builds: - -```sh -cargo install -f wasm-bindgen-cli@0.2.100 -cargo install wasm-pack -cargo install cargo-watch -cargo install cargo-about -``` - -Regarding the last one: you'll likely get faster build times if you manually install that specific version of `wasm-bindgen-cli`. It is supposed to be installed automatically but a version mismatch causes it to reinstall every single recompilation. It may need to be manually updated periodically to match the version of the `wasm-bindgen` dependency in [`Cargo.toml`](https://github.com/GraphiteEditor/Graphite/blob/master/Cargo.toml). - ## Repository Clone the project to a convenient location: @@ -40,7 +29,7 @@ git clone https://github.com/GraphiteEditor/Graphite.git From either the `/` (root) or `/frontend` directories, you can run the project by executing: ```sh -npm start +cargo run ``` This spins up the dev server at with a file watcher that performs hot reloading of the web page. You should be able to start the server, edit and save web and Rust code, and shut it down by double pressing CtrlC. TypeScript and HTML changes require a manual page reload to fix broken state. @@ -53,28 +42,9 @@ This method compiles Graphite code in debug mode which includes debug symbols fo On rare occasions (like while running advanced performance profiles or proxying the dev server connection over a slow network where the >100 MB unoptimized binary size would pose an issue), you may need to run the dev server with release optimizations. To do that while keeping debug symbols: ```sh -npm run profiling +cargo run release ``` -To run the dev server without debug symbols, using the same release optimizations as production builds: - -```sh -npm run production -``` - - - -
-Production build instructions: click here - -You'll rarely need to compile your own production builds because our CI/CD system takes care of deployments. However, you can compile a production build with full optimizations by running: - -```sh -npm run build -``` - -This produces the `/frontend/dist` directory containing the static site files that must be served by your own web server. -
## Development tooling