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
This commit is contained in:
Timon 2026-03-07 14:26:19 +01:00 committed by GitHub
parent 50ef6e15cb
commit 5d22292072
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 664 additions and 404 deletions

2
.envrc
View File

@ -1 +1 @@
use flake .nix
use flake

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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() }}

View File

@ -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

View File

@ -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: |

81
.nix/default.nix Normal file
View File

@ -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);
}

View File

@ -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);
}
);
}

View File

@ -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

8
Cargo.lock generated
View File

@ -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]]

View File

@ -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" }

View File

@ -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<PathBuf, Box<dyn Error>> {
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<PathBuf, Box
pub(crate) fn run_command(program: &str, args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
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(())
}

View File

@ -1,20 +1,19 @@
use std::error::Error;
use crate::common::*;
pub fn main() -> Result<(), Box<dyn Error>> {
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
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<String> = 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(())

View File

@ -22,9 +22,11 @@ pub fn main() -> Result<(), Box<dyn Error>> {
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<String> = 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(())

View File

@ -12,9 +12,10 @@ pub fn main() -> Result<(), Box<dyn Error>> {
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<String> = 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(())

View File

@ -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<H: CefEventHandler> CefContextBuilder<H> {
});
}
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<H: CefEventHandler> CefContextBuilder<H> {
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<H: CefEventHandler>(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,
}

View File

@ -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() {

View File

@ -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"
}

25
flake.nix Normal file
View File

@ -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;
}

View File

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

View File

@ -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)',

View File

@ -1,3 +0,0 @@
[profile.profiling]
inherits = "release"
debug = true

View File

@ -5,7 +5,7 @@ use std::path::PathBuf;
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
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(());

6
package-lock.json generated
View File

@ -1,6 +0,0 @@
{
"name": "Graphite",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}

View File

@ -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"
}
}

View File

@ -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 }

104
tools/cargo-run/src/lib.rs Normal file
View File

@ -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<String>,
}
impl Task {
pub fn parse(args: &[&str]) -> Option<Self> {
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::<Vec<_>>();
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),
}

View File

@ -0,0 +1,99 @@
use std::process::ExitCode;
use cargo_run::*;
fn usage() {
println!();
println!("USAGE:");
println!(" cargo run [<command>] [<target>] [<profile>] [-- [args]...]");
println!();
println!("COMMON USAGE:");
println!(" cargo run Run the web app");
println!(" cargo run desktop Run the desktop app");
println!();
println!("OPTIONS:");
println!("<command>:");
println!(" [run] Run the selected target (default)");
println!(" build Build the selected target");
println!(" help Show this message");
println!("<target>:");
println!(" [web] Web app (default)");
println!(" desktop Desktop app");
println!(" cli Graphene CLI");
println!("<profile>:");
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<String> = 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(())
}

View File

@ -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> {
[
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<Requirement> = Vec::new();
let mut failures: Vec<String> = 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(())
}

View File

@ -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 }

View File

@ -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<LicenseEntry> {
parse(run())
fn licenses(&self) -> Result<Vec<LicenseEntry>, Error> {
Ok(parse(run()?))
}
}
@ -84,23 +84,18 @@ fn parse(parsed: Output) -> Vec<LicenseEntry> {
.collect()
}
fn run() -> Output {
fn run() -> Result<Output, Error> {
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()))
}

View File

@ -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<LicenseEntry> {
let html = read();
parse(&html)
fn licenses(&self) -> Result<Vec<LicenseEntry>, Error> {
let html = read()?;
Ok(parse(&html))
}
}
impl Hash for CefLicenseSource {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
read().hash(state)
read().unwrap().hash(state)
}
}
@ -64,42 +64,29 @@ fn parse(html: &str) -> Vec<LicenseEntry> {
.collect()
}
fn read() -> String {
fn read() -> Result<String, Error> {
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())))
}
}

View File

@ -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<LicenseEntry>;
fn licenses(&self) -> Result<Vec<LicenseEntry>, 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(&current_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(&current_hash_path, hash).unwrap_or_else(|e| {
eprintln!("Failed to write hash file {}: {e}", current_hash_path.display());
process::exit(1);
});
fs::write(&current_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<LicenseEntry>) -> String {
@ -210,20 +231,11 @@ fn dedup_by_licence_text(vec: Vec<LicenseEntry>) -> Vec<LicenseEntry> {
}
#[cfg(feature = "desktop")]
fn compress(content: &str) -> Vec<u8> {
fn compress(content: &str) -> Result<Vec<u8>, 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)
}

View File

@ -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<LicenseEntry> {
parse(run(&self.dir))
fn licenses(&self) -> Result<Vec<LicenseEntry>, Error> {
Ok(parse(run(&self.dir)?))
}
}
@ -66,7 +65,7 @@ fn parse(parsed: Output) -> Vec<LicenseEntry> {
.collect()
}
fn run(dir: &std::path::Path) -> Output {
fn run(dir: &std::path::Path) -> Result<Output, Error> {
#[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()))
}

View File

@ -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 <http://localhost:8080> 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 <kbd>Ctrl</kbd><kbd>C</kbd>. 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
```
</details>
<details>
<summary>Production build instructions: click here</summary>
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.
</details>
## Development tooling