diff --git a/.github/workflows/build-linux-bundle.yml b/.github/workflows/build-linux-bundle.yml index 9ca2334ade..d5584f3542 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 826e5f0aa8..08f9dd043f 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 desktop build - name: Stage Artifacts shell: bash diff --git a/.github/workflows/build-nix-package.yml b/.github/workflows/build-nix-package.yml index 2da86b2d34..45669d0309 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 1be2e3e947..b01f090e92 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 web build - name: 📤 Publish to Cloudflare Pages id: cloudflare diff --git a/.github/workflows/build-win-bundle.yml b/.github/workflows/build-win-bundle.yml index ee72140db8..49983af141 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 desktop build - name: Stage Artifacts shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63621a0f1e..1a7558887f 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 web build - 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 f2ec321758..e7cbdd93f8 100644 --- a/.github/workflows/comment-!build-commands.yml +++ b/.github/workflows/comment-!build-commands.yml @@ -82,10 +82,10 @@ 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 + if [[ "${{ github.event.comment.body }}" == "!build-debug" ]]; then + echo "command=build debug" >> $GITHUB_OUTPUT elif [[ "${{ github.event.comment.body }}" == "!build-profiling" ]]; then - echo "command=build-profiling" >> $GITHUB_OUTPUT + echo "command=build profiling" >> $GITHUB_OUTPUT elif [[ "${{ github.event.comment.body }}" == "!build" ]]; then echo "command=build" >> $GITHUB_OUTPUT else @@ -108,9 +108,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 web ${{ 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 852af0ad03..57f5cc2676 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 web build - name: 📤 Publish to Cloudflare Pages id: cloudflare diff --git a/.github/workflows/provide-shaders.yml b/.github/workflows/provide-shaders.yml index 7e38ecf303..b6006a7b24 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 0000000000..e77cc4a60d --- /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 270bb870b2..0000000000 --- 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 5b13af2e20..0000000000 --- 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 6bf8977a92..9f41a56e25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -609,6 +609,10 @@ dependencies = [ "tokio", ] +[[package]] +name = "building" +version = "0.0.0" + [[package]] name = "built" version = "0.7.7" @@ -6250,6 +6254,7 @@ dependencies = [ "scraper", "serde", "serde_json", + "thiserror 2.0.18", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6c8c121b88..2e5d58042f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "node-graph/node-macro", "node-graph/preprocessor", "proc-macros", + "tools/building", "tools/crate-hierarchy-viz", "tools/third-party-licenses", "tools/editor-message-tree", @@ -71,13 +72,13 @@ default-members = [ "node-graph/nodes/transform", "node-graph/nodes/vector", "node-graph/graph-craft", - "node-graph/graphene-cli", "node-graph/nodes/gstd", "node-graph/interpreted-executor", "node-graph/node-macro", "node-graph/preprocessor", # blocked by https://github.com/rust-lang/cargo/issues/15890 # "proc-macros", + "tools/building", ] resolver = "2" diff --git a/desktop/bundle/src/common.rs b/desktop/bundle/src/common.rs index 3e0523b878..4ae8a5df26 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}; @@ -45,7 +47,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); + panic!("Command '{}' with args {:?} failed with status: {}", program, args, status); } Ok(()) } diff --git a/desktop/bundle/src/linux.rs b/desktop/bundle/src/linux.rs index 94cd480ee3..fb5c462ffe 100644 --- a/desktop/bundle/src/linux.rs +++ b/desktop/bundle/src/linux.rs @@ -1,8 +1,6 @@ -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 @@ -11,7 +9,7 @@ pub fn main() -> Result<(), Box> { if std::env::args().any(|a| a == "open") { run_command(&app_bin.to_string_lossy(), &[]).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); diff --git a/desktop/src/cef/context/builder.rs b/desktop/src/cef/context/builder.rs index e6ad318688..972a4fa382 100644 --- a/desktop/src/cef/context/builder.rs +++ b/desktop/src/cef/context/builder.rs @@ -138,8 +138,7 @@ impl CefContextBuilder { }); } Err(e) => { - tracing::error!("Failed to initialize CEF context: {:?}", e); - std::process::exit(1); + panic!("Failed to initialize CEF context: {:?}", e); } }); diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index f56245e03f..9fa83cbd64 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); } }; @@ -87,20 +85,16 @@ pub fn start() { context } Err(cef::InitError::AlreadyRunning) => { - tracing::error!("Another instance is already running, Exiting."); - exit(1); + panic!("Another instance is already running."); } Err(cef::InitError::InitializationFailed(code)) => { - tracing::error!("Cef initialization failed with code: {code}"); - exit(1); + 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 +133,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 7bb7e2f254..fbc34b8adb 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 0000000000..04f0e80fea --- /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/package-lock.json b/package-lock.json deleted file mode 100644 index a8f7238008..0000000000 --- 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 b49c6ca203..0000000000 --- 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/building/Cargo.toml b/tools/building/Cargo.toml new file mode 100644 index 0000000000..fce23d009b --- /dev/null +++ b/tools/building/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "building" +edition.workspace = true +version.workspace = true +license.workspace = true +authors.workspace = true + +default-run = "building" diff --git a/tools/building/src/checks.rs b/tools/building/src/checks.rs new file mode 100644 index 0000000000..42cc8bcbaa --- /dev/null +++ b/tools/building/src/checks.rs @@ -0,0 +1,93 @@ +use std::process::Command; + +struct Dependency { + command: &'static str, + args: &'static [&'static str], + name: &'static str, + expected_version: Option<&'static str>, +} + +const DEPENDENCIES: &[Dependency] = &[ + Dependency { + command: "rustc", + args: &["--version"], + name: "Rust", + expected_version: None, + }, + Dependency { + command: "cargo-about", + args: &["--version"], + name: "cargo-about", + expected_version: None, + }, + Dependency { + command: "cargo-watch", + args: &["--version"], + name: "cargo-watch", + expected_version: None, + }, + Dependency { + command: "wasm-bindgen", + args: &["--version"], + name: "wasm-bindgen-cli", + expected_version: Some("0.2.100"), + }, + Dependency { + command: "wasm-pack", + args: &["--version"], + name: "wasm-pack", + expected_version: None, + }, + Dependency { + command: "node", + args: &["--version"], + name: "Node.js", + expected_version: None, + }, +]; + +pub fn check_dependencies() { + let mut failures = Vec::new(); + + for dep in DEPENDENCIES { + match Command::new(dep.command).args(dep.args).output() { + Ok(output) if output.status.success() => { + let version_output = String::from_utf8_lossy(&output.stdout); + let version = version_output.trim(); + + if let Some(expected) = dep.expected_version { + if version.contains(expected) { + eprintln!(" ✓ {} ({})", dep.name, version); + } else { + eprintln!(" ✗ {} (found {}, expected {})", dep.name, version, expected); + failures.push(format!("{}: version mismatch (found {}, expected {})", dep.name, version, expected)); + } + } else { + eprintln!(" ✓ {} ({})", dep.name, version); + } + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!(" ✗ {} — command failed: {}", dep.name, stderr.trim()); + failures.push(format!("{}: not installed or not working", dep.name)); + } + Err(_) => { + eprintln!(" ✗ {} — not found", dep.name); + failures.push(format!("{}: not found in PATH", dep.name)); + } + } + } + + eprintln!(); + + if failures.is_empty() { + eprintln!("All dependencies are installed."); + } else { + eprintln!("{} missing or misconfigured dependenc{}:", failures.len(), if failures.len() == 1 { "y" } else { "ies" }); + for failure in &failures { + eprintln!(" - {failure}"); + } + eprintln!(); + eprintln!("See: https://graphite.rs/volunteer/guide/project-setup/"); + } +} diff --git a/tools/building/src/lib.rs b/tools/building/src/lib.rs new file mode 100644 index 0000000000..e784a00ae9 --- /dev/null +++ b/tools/building/src/lib.rs @@ -0,0 +1,56 @@ +pub mod checks; + +use std::process; + +pub enum Profile { + Default, + Release, + Debug, + Profiling, + Error, +} + +impl From<&[&str]> for Profile { + fn from(arg: &[&str]) -> Self { + arg.first().map(|s| s.to_string()).as_deref().unwrap_or_default().into() + } +} + +impl From<&str> for Profile { + fn from(arg: &str) -> Self { + match arg { + "release" => Profile::Release, + "debug" => Profile::Debug, + "profiling" => Profile::Profiling, + _ if arg.is_empty() => Profile::Default, + _ => Profile::Error, + } + } +} + +pub fn run(comand: &str) { + run_from(comand, None); +} + +pub fn run_in_frontend_dir(comand: &str) { + run_from(comand, Some("frontend")); +} + +pub fn run_from(comand: &str, dir: Option<&str>) { + let workspace_dir = std::path::PathBuf::from(env!("CARGO_WORKSPACE_DIR")); + let dir = if let Some(dir) = dir { workspace_dir.join(dir) } else { workspace_dir }; + let comand = comand.split_whitespace().collect::>(); + let mut cmd = process::Command::new(comand[0]); + if comand.len() > 1 { + cmd.args(&comand[1..]); + } + cmd.current_dir(dir); + cmd.spawn() + .unwrap_or_else(|e| { + panic!("Failed to run command '{}': {e}", comand.join(" ")); + }) + .wait() + .unwrap_or_else(|e| { + panic!("Failed to wait for command '{}': {e}", comand.join(" ")); + }); +} diff --git a/tools/building/src/main.rs b/tools/building/src/main.rs new file mode 100644 index 0000000000..98feae72aa --- /dev/null +++ b/tools/building/src/main.rs @@ -0,0 +1,84 @@ +use building::*; + +fn usage() { + eprintln!("usage: cargo run [] [release|debug|profiling]"); + eprintln!(); + eprintln!("commands:"); + eprintln!(" web Run the dev server"); + eprintln!(" web build Build the web version"); + eprintln!(" desktop Run the desktop app"); + eprintln!(" desktop build Build the desktop version"); + eprintln!(" check Check that all required dependencies are installed"); +} + +fn main() { + let args: Vec = std::env::args().collect(); + let args: Vec<&str> = args.iter().skip(1).map(String::as_str).collect(); + + match args.as_slice() { + ["desktop", rest @ ..] => match rest { + ["build", rest @ ..] => build_desktop(rest.into()), + _ => run_desktop(rest.into()), + }, + ["web", rest @ ..] => match rest { + ["build", rest @ ..] => build_web(rest.into()), + _ => run_web(rest.into()), + }, + rest => match rest { + ["build", rest @ ..] => build_web(rest.into()), + _ => run_web(rest.into()), + }, + } +} + +fn run_web(profile: Profile) { + match profile { + Profile::Debug | Profile::Default => run_in_frontend_dir("npm run start"), + Profile::Release => run_in_frontend_dir("npm run production"), + Profile::Profiling => run_in_frontend_dir("npm run profiling"), + Profile::Error => usage(), + } +} + +fn run_desktop(profile: Profile) { + match profile { + Profile::Debug | Profile::Default => { + run_in_frontend_dir("npm run build-native-dev"); + run("cargo run -p third-party-licenses --features desktop"); + run("cargo run -p graphite-desktop-bundle -- open"); + } + Profile::Release => { + run_in_frontend_dir("npm run build-native"); + run("cargo run -p third-party-licenses --features desktop"); + run("cargo run -r -p graphite-desktop-bundle -- open"); + } + Profile::Profiling => todo!("profiling run for desktop"), + Profile::Error => usage(), + } +} + +fn build_web(profile: Profile) { + match profile { + Profile::Debug => run_in_frontend_dir("npm run build-dev"), + Profile::Release | Profile::Default => run_in_frontend_dir("npm run build"), + Profile::Profiling => run_in_frontend_dir("npm run build-profiling"), + Profile::Error => usage(), + } +} + +fn build_desktop(profile: Profile) { + match profile { + Profile::Debug => { + run_in_frontend_dir("npm run build-native-dev"); + run("cargo run -p third-party-licenses --features desktop"); + run("cargo run -p graphite-desktop-bundle"); + } + Profile::Release | Profile::Default => { + run_in_frontend_dir("npm run build-native"); + run("cargo run -p third-party-licenses --features desktop"); + run("cargo run -r -p graphite-desktop-bundle"); + } + Profile::Profiling => todo!("profiling build for desktop"), + Profile::Error => usage(), + } +} diff --git a/tools/third-party-licenses/Cargo.toml b/tools/third-party-licenses/Cargo.toml index 4a59967389..fe348963c9 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 49d6731dd4..7b10a4fa43 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 94fe45d08d..b2eaa82755 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 afa1b501f0..6f5ca2f2ff 100644 --- a/tools/third-party-licenses/src/main.rs +++ b/tools/third-party-licenses/src/main.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; +use std::fs; use std::hash::{DefaultHasher, Hash, Hasher}; use std::path::PathBuf; -use std::{fs, process}; mod cargo; #[cfg(feature = "desktop")] @@ -13,8 +13,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 { @@ -39,6 +58,13 @@ struct Run<'a> { } fn main() { + if let Err(e) = run() { + eprintln!("Error: {e}"); + std::process::exit(1); + } +} + +fn run() -> Result<(), Error> { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let workspace_dir = PathBuf::from(env!("CARGO_WORKSPACE_DIR")); @@ -71,32 +97,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 +125,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 +229,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 aac94f5cad..01db5003dc 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 3934df3a20..bb4b32c8d7 100644 --- a/website/content/volunteer/guide/project-setup/_index.md +++ b/website/content/volunteer/guide/project-setup/_index.md @@ -40,7 +40,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,13 +53,13 @@ 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 profiling ``` To run the dev server without debug symbols, using the same release optimizations as production builds: ```sh -npm run production +cargo run release ``` @@ -70,7 +70,7 @@ npm run production 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 +cargo run web build ``` This produces the `/frontend/dist` directory containing the static site files that must be served by your own web server.