diff --git a/.branding b/.branding new file mode 100644 index 0000000000..7f1c9ecbe7 --- /dev/null +++ b/.branding @@ -0,0 +1,2 @@ +https://github.com/Keavon/graphite-branded-assets/archive/f44aa2f362ae4fed8d634878b817a1d3948a7dcb.tar.gz +dffe2b483e491979ef57c320d61446ada5400ef73ff26582976631d9c36efefc diff --git a/.cargo/config.toml b/.cargo/config.toml index 4087f77f96..b6ad2fee27 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -10,3 +10,6 @@ rustflags = [ "link-arg=--max-memory=4294967296", "--cfg=web_sys_unstable_apis", ] + +[env] +CARGO_WORKSPACE_DIR = { value = "", relative = true } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6b4ee383f2..bddfffaf1f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -23,7 +23,6 @@ "streetsidesoftware.code-spell-checker", // Helpful "mhutchie.git-graph", - "waderyan.gitblame", "qezhu.gitlink", "wmaurer.change-case" ] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4662c5521a..b758bce647 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,13 @@ - + diff --git a/.github/workflows/build-dev-and-ci.yml b/.github/workflows/build-dev-and-ci.yml index 016d42cbb3..c903568c0b 100644 --- a/.github/workflows/build-dev-and-ci.yml +++ b/.github/workflows/build-dev-and-ci.yml @@ -5,9 +5,10 @@ on: branches: - master pull_request: {} + merge_group: {} env: CARGO_TERM_COLOR: always - INDEX_HTML_HEAD_REPLACEMENT: + INDEX_HTML_HEAD_REPLACEMENT: jobs: build: @@ -34,10 +35,10 @@ jobs: with: node-version: "latest" - - name: ๐Ÿšง Install Node dependencies + - name: ๐Ÿšง Install build dependencies run: | cd frontend - npm ci + npm run setup - name: ๐Ÿฆ€ Install the latest Rust run: | @@ -47,6 +48,11 @@ jobs: echo "Latest updated version:" rustc --version + - name: ๐Ÿฆ€ Fetch Rust dependencies + run: | + echo "If it fails here, the committed Cargo.lock may be out of date" + cargo fetch --locked + - name: โœ‚ Replace template in of index.html run: | # Remove the INDEX_HTML_HEAD_REPLACEMENT environment variable for build links (not master deploys) @@ -103,14 +109,16 @@ jobs: - name: ๐Ÿงช Run Rust tests run: | - mold -run cargo test --all-features --workspace + mold -run cargo test --all-features - name: ๐Ÿ“ƒ Generate code documentation info for website if: github.ref == 'refs/heads/master' run: | - cargo test --package graphite-editor --lib -- messages::message::test::generate_message_tree + cd tools/editor-message-tree + cargo run + cd ../.. mkdir -p artifacts-generated - mv hierarchical_message_system_tree.txt artifacts-generated/hierarchical_message_system_tree.txt + mv website/generated/hierarchical_message_system_tree.txt artifacts-generated/hierarchical_message_system_tree.txt - name: ๐Ÿ’ฟ Obtain cache of auto-generated code docs artifacts, to check if they've changed if: github.ref == 'refs/heads/master' diff --git a/.github/workflows/build-linux-bundle.yml b/.github/workflows/build-linux-bundle.yml new file mode 100644 index 0000000000..4b07e490b2 --- /dev/null +++ b/.github/workflows/build-linux-bundle.yml @@ -0,0 +1,59 @@ +name: Build Linux Bundle + +on: + workflow_dispatch: {} + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main + + - name: Free disk space + run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache + + - name: Build Linux Bundle + run: nix build .nix#graphite-bundle.tar.xz && cp ./result ./graphite-linux-bundle.tar.xz + + - name: Upload Linux Bundle + uses: actions/upload-artifact@v4 + with: + name: graphite-linux-bundle + path: graphite-linux-bundle.tar.xz + compression-level: 0 + + - name: Setup Flatpak Tooling + run: | + sudo apt-get update + sudo apt-get install -y flatpak flatpak-builder + flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + + - name: Build Flatpak + run: | + nix build .nix#graphite-flatpak-manifest + + rm -rf .flatpak + mkdir -p .flatpak + + cp ./result .flatpak/manifest.json + + cd .flatpak + mkdir -p repo + + flatpak-builder --user --force-clean --install-deps-from=flathub --repo=repo build ./manifest.json + + flatpak build-bundle repo graphite.flatpak art.graphite.Graphite --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo + + - name: Upload Flatpak + uses: actions/upload-artifact@v4 + with: + name: graphite-flatpak + path: .flatpak/graphite.flatpak + compression-level: 0 diff --git a/.github/workflows/build-mac-bundle.yml b/.github/workflows/build-mac-bundle.yml new file mode 100644 index 0000000000..ee6d1bbb3b --- /dev/null +++ b/.github/workflows/build-mac-bundle.yml @@ -0,0 +1,153 @@ +name: Build Mac Bundle + +on: + push: + branches: + - master + +jobs: + build: + runs-on: macos-latest + + env: + WASM_BINDGEN_CLI_VERSION: "0.2.100" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + override: true + rustflags: "" + target: wasm32-unknown-unknown + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + frontend/package-lock.json + + - name: Install Native Dependencies + env: + GITHUB_TOKEN: ${{ github.token }} + BINSTALL_DISABLE_TELEMETRY: "true" + run: | + brew update + brew install \ + pkg-config \ + openssl@3 \ + binaryen \ + llvm \ + cargo-binstall + + echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV + echo "$(brew --prefix llvm)/bin" >> $GITHUB_PATH + + cargo binstall --no-confirm --force wasm-pack + cargo binstall --no-confirm --force cargo-about + cargo binstall --no-confirm --force "wasm-bindgen-cli@${WASM_BINDGEN_CLI_VERSION}" + + - name: Build Mac Bundle + env: + CARGO_TERM_COLOR: always + run: npm run build-desktop + + - name: Stage Artifacts + shell: bash + run: | + rm -rf target/artifacts + mkdir -p target/artifacts + cp -R target/release/Graphite.app target/artifacts/Graphite.app + + - name: Upload Mac Bundle + uses: actions/upload-artifact@v4 + with: + name: graphite-mac-bundle + path: target/artifacts + + - name: Sign and Notarize Mac Bundle Preparation + env: + APPLE_CERT_BASE64: ${{ secrets.APPLE_CERT_BASE64 }} + APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} + run: | + mkdir -p .sign + echo "$APPLE_CERT_BASE64" | base64 --decode > .sign/certificate.p12 + + security create-keychain -p "" .sign/main.keychain + security default-keychain -s .sign/main.keychain + security unlock-keychain -p "" .sign/main.keychain + security set-keychain-settings -t 3600 -u .sign/main.keychain + + security import .sign/certificate.p12 -k .sign/main.keychain -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign + security set-key-partition-list -S apple-tool:,apple: -s -k "" .sign/main.keychain + + cat > .sign/entitlements.plist <<'EOF' + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-executable-page-protection + + com.apple.security.cs.disable-library-validation + + + + EOF + + - name: Sign and Notarize Mac Bundle + env: + APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_CERT_NAME: ${{ secrets.APPLE_CERT_NAME }} + run: | + CERTIFICATE="$APPLE_CERT_NAME" + ENTITLEMENTS=".sign/entitlements.plist" + APP_PATH="target/artifacts/Graphite.app" + ZIP_PATH=".sign/Graphite.zip" + + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper.app" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (GPU).app" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (Renderer).app" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libcef_sandbox.dylib" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libEGL.dylib" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH" --deep + + codesign --verify --deep --strict --verbose=4 "$APP_PATH" + + ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH" + xcrun notarytool submit "$ZIP_PATH" --wait --apple-id "$APPLE_EMAIL" --team-id "$APPLE_TEAM_ID" --password "$APPLE_PASSWORD" + rm "$ZIP_PATH" + + xcrun stapler staple -v "$APP_PATH" + + spctl -a -vv "$APP_PATH" + + - name: Upload Mac Bundle Signed + uses: actions/upload-artifact@v4 + with: + name: graphite-mac-bundle-signed + path: target/artifacts diff --git a/.github/workflows/build-nix-package.yml b/.github/workflows/build-nix-package.yml new file mode 100644 index 0000000000..2da86b2d34 --- /dev/null +++ b/.github/workflows/build-nix-package.yml @@ -0,0 +1,17 @@ +name: Build Nix Package + +on: + workflow_dispatch: {} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main + + - name: Build Nix Package Dev + run: nix build .nix#graphite-dev --print-build-logs diff --git a/.github/workflows/build-production.yml b/.github/workflows/build-production.yml index fa58e1865b..1be2e3e947 100644 --- a/.github/workflows/build-production.yml +++ b/.github/workflows/build-production.yml @@ -18,7 +18,7 @@ jobs: RUSTC_WRAPPER: /usr/bin/sccache CARGO_INCREMENTAL: 0 SCCACHE_DIR: /var/lib/github-actions/.cache - INDEX_HTML_HEAD_REPLACEMENT: + INDEX_HTML_HEAD_REPLACEMENT: steps: - name: ๐Ÿ“ฅ Clone and checkout repository @@ -32,10 +32,10 @@ jobs: with: node-version: "latest" - - name: ๐Ÿšง Install Node dependencies + - name: ๐Ÿšง Install build dependencies run: | cd frontend - npm ci + npm run setup - name: ๐Ÿฆ€ Install the latest Rust run: | diff --git a/.github/workflows/build-win-bundle.yml b/.github/workflows/build-win-bundle.yml new file mode 100644 index 0000000000..173efbf866 --- /dev/null +++ b/.github/workflows/build-win-bundle.yml @@ -0,0 +1,167 @@ +name: Build Windows Bundle + +on: + push: + branches: + - master + +permissions: + contents: read + id-token: write + +jobs: + build: + runs-on: windows-latest + + env: + WASM_BINDGEN_CLI_VERSION: "0.2.100" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + override: true + rustflags: "" + target: wasm32-unknown-unknown + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ${{ env.USERPROFILE }}\.cargo\registry + ${{ env.USERPROFILE }}\.cargo\git + target + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + frontend/package-lock.json + + - name: Setup Cargo Binstall + uses: cargo-bins/cargo-binstall@main + + - name: Install Native Dependencies + shell: pwsh + env: + GITHUB_TOKEN: ${{ github.token }} + BINSTALL_DISABLE_TELEMETRY: "true" + run: | + winget install --id LLVM.LLVM -e --accept-package-agreements --accept-source-agreements + winget install --id Kitware.CMake -e --accept-package-agreements --accept-source-agreements + winget install --id OpenSSL.OpenSSL -e --accept-package-agreements --accept-source-agreements + winget install --id WebAssembly.Binaryen -e --accept-package-agreements --accept-source-agreements + winget install --id GnuWin32.PkgConfig -e --accept-package-agreements --accept-source-agreements + + "OPENSSL_DIR=C:\Program Files\OpenSSL-Win64" | Out-File -FilePath $env:GITHUB_ENV -Append + "PKG_CONFIG_PATH=C:\Program Files\OpenSSL-Win64\lib\pkgconfig" | Out-File -FilePath $env:GITHUB_ENV -Append + + cargo binstall --no-confirm --force wasm-pack + cargo binstall --no-confirm --force cargo-about + cargo binstall --no-confirm --force "wasm-bindgen-cli@$env:WASM_BINDGEN_CLI_VERSION" + + - name: Build Windows Bundle + env: + CARGO_TERM_COLOR: always + run: npm run build-desktop + + - name: Stage Artifacts + shell: bash + run: | + rm -rf target/artifacts + mkdir -p target/artifacts + cp -R target/release/Graphite target/artifacts/Graphite + + - name: Upload Windows Bundle + uses: actions/upload-artifact@v4 + with: + name: graphite-windows-bundle + path: target/artifacts + + - name: Azure login + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Sign + uses: azure/artifact-signing-action@v1 + with: + endpoint: https://eus.codesigning.azure.net/ + signing-account-name: Graphite + certificate-profile-name: Graphite + files: | + ${{ github.workspace }}\target\artifacts\Graphite\Graphite.exe + ${{ github.workspace }}\target\artifacts\Graphite\libcef.dll + ${{ github.workspace }}\target\artifacts\Graphite\chrome_elf.dll + ${{ github.workspace }}\target\artifacts\Graphite\vulkan-1.dll + ${{ github.workspace }}\target\artifacts\Graphite\dxcompiler.dll + ${{ github.workspace }}\target\artifacts\Graphite\libEGL.dll + ${{ github.workspace }}\target\artifacts\Graphite\libGLESv2.dll + ${{ github.workspace }}\target\artifacts\Graphite\vk_swiftshader.dll + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + correlation-id: ${{ github.sha }} + + - name: Verify Signatures + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + + $TargetDir = "target\artifacts\Graphite" + + if (-not (Test-Path $TargetDir)) { + throw "TargetDir not found: $TargetDir" + } + + $UnsignedOrBad = @() + + Get-ChildItem -Path $TargetDir -Recurse -File -Include *.exe,*.dll | ForEach-Object { + $sig = Get-AuthenticodeSignature -FilePath $_.FullName + + if ($sig.Status -ne 'Valid') { + $UnsignedOrBad += "$($_.FullName) (Status=$($sig.Status))" + } + } + + if ($UnsignedOrBad.Count -gt 0) { + Write-Host "Unsigned or invalid binaries detected:" + $UnsignedOrBad | ForEach-Object { + Write-Host "::error::$_" + } + + if ($env:GITHUB_STEP_SUMMARY) { + "### โŒ Unsigned or invalid binaries detected" | + Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + "" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + $UnsignedOrBad | ForEach-Object { + "* `$_" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + } + } + + exit 1 + } + + Write-Host "All binaries are signed and valid." + + if ($env:GITHUB_STEP_SUMMARY) { + "### โœ… All binaries are signed and valid" | + Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + } + + - name: Upload Windows Bundle Signed + uses: actions/upload-artifact@v4 + with: + name: graphite-windows-bundle-signed + path: target/artifacts diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml index 47712103fa..4710c15554 100644 --- a/.github/workflows/cargo-deny.yml +++ b/.github/workflows/cargo-deny.yml @@ -7,6 +7,7 @@ on: jobs: cargo-deny: + if: github.repository == 'GraphiteEditor/Graphite' # Don't run on forks by default runs-on: ubuntu-latest steps: diff --git a/.github/workflows/comment-!build-commands.yml b/.github/workflows/comment-!build-commands.yml index 8d8a0ae096..87b8581b19 100644 --- a/.github/workflows/comment-!build-commands.yml +++ b/.github/workflows/comment-!build-commands.yml @@ -59,10 +59,10 @@ jobs: with: node-version: "latest" - - name: ๐Ÿšง Install Node dependencies + - name: ๐Ÿšง Install build dependencies run: | cd frontend - npm ci + npm run setup - name: ๐Ÿฆ€ Install the latest Rust run: | diff --git a/.github/workflows/comment-profiling-changes.yaml b/.github/workflows/comment-profiling-changes.yaml index e51341538c..76b12265a1 100644 --- a/.github/workflows/comment-profiling-changes.yaml +++ b/.github/workflows/comment-profiling-changes.yaml @@ -2,6 +2,10 @@ name: Profiling Changes on: pull_request: + paths: + - 'node-graph/**' + - 'Cargo.toml' + - 'Cargo.lock' env: CARGO_TERM_COLOR: always @@ -9,6 +13,7 @@ env: jobs: profile: runs-on: ubuntu-latest + continue-on-error: true steps: - uses: actions/checkout@v4 with: @@ -33,12 +38,12 @@ jobs: uses: actions/cache@v4 with: path: ~/.cargo/bin/iai-callgrind-runner - key: ${{ runner.os }}-iai-callgrind-runner-0.12.3 + key: ${{ runner.os }}-iai-callgrind-runner-0.16.1 - name: Install iai-callgrind if: steps.cache-iai.outputs.cache-hit != 'true' run: | - cargo install iai-callgrind-runner@0.12.3 + cargo install iai-callgrind-runner@0.16.1 - name: Checkout master branch run: | @@ -49,21 +54,30 @@ jobs: id: master-sha run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + - name: Get CPU info + id: cpu-info + run: | + # Get CPU model and create a short hash for cache key + CPU_MODEL=$(cat /proc/cpuinfo | grep "model name" | head -1 | cut -d: -f2 | xargs) + CPU_HASH=$(echo "$CPU_MODEL" | sha256sum | cut -c1-8) + echo "cpu-hash=$CPU_HASH" >> $GITHUB_OUTPUT + echo "CPU: $CPU_MODEL (hash: $CPU_HASH)" + - name: Cache benchmark baselines id: cache-benchmark-baselines uses: actions/cache@v4 with: path: target/iai - key: ${{ runner.os }}-benchmark-baselines-master-${{ steps.master-sha.outputs.sha }} + key: ${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-info.outputs.cpu-hash }}-benchmark-baselines-master-${{ steps.master-sha.outputs.sha }} restore-keys: | - ${{ runner.os }}-benchmark-baselines-master- + ${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-info.outputs.cpu-hash }}-benchmark-baselines-master- - name: Run baseline benchmarks if: steps.cache-benchmark-baselines.outputs.cache-hit != 'true' run: | # Compile benchmarks cargo bench --bench compile_demo_art_iai -- --save-baseline=master - + # Runtime benchmarks cargo bench --bench update_executor_iai -- --save-baseline=master cargo bench --bench run_once_iai -- --save-baseline=master @@ -74,34 +88,18 @@ jobs: git checkout ${{ github.event.pull_request.head.sha }} - name: Run PR benchmarks - id: benchmark run: | # Compile benchmarks - COMPILE_OUTPUT=$(cargo bench --bench compile_demo_art_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g') - - # Runtime benchmarks - UPDATE_OUTPUT=$(cargo bench --bench update_executor_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g') - RUN_ONCE_OUTPUT=$(cargo bench --bench run_once_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g') - RUN_CACHED_OUTPUT=$(cargo bench --bench run_cached_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g') - - # Store outputs - echo "COMPILE_OUTPUT<> $GITHUB_OUTPUT - echo "$COMPILE_OUTPUT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - echo "UPDATE_OUTPUT<> $GITHUB_OUTPUT - echo "$UPDATE_OUTPUT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - echo "RUN_ONCE_OUTPUT<> $GITHUB_OUTPUT - echo "$RUN_ONCE_OUTPUT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - echo "RUN_CACHED_OUTPUT<> $GITHUB_OUTPUT - echo "$RUN_CACHED_OUTPUT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + cargo bench --bench compile_demo_art_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/compile_output.json + + # Runtime benchmarks + cargo bench --bench update_executor_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/update_output.json + cargo bench --bench run_once_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_once_output.json + cargo bench --bench run_cached_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_cached_output.json - name: Make old comments collapsed by default + # Only run if we have write permissions (not a fork) + if: github.event.pull_request.head.repo.full_name == github.repository uses: actions/github-script@v7 with: github-token: ${{secrets.GITHUB_TOKEN}} @@ -126,17 +124,79 @@ jobs: }); } + - name: Analyze profiling changes + id: analyze + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + function isSignificantChange(diffPct, absoluteChange, benchmarkType) { + const meetsPercentageThreshold = Math.abs(diffPct) > 5; + const meetsAbsoluteThreshold = absoluteChange > 200000; + const isCachedExecution = benchmarkType === 'run_cached' || + benchmarkType.includes('Cached Execution'); + + return isCachedExecution + ? (meetsPercentageThreshold && meetsAbsoluteThreshold) + : meetsPercentageThreshold; + } + + const allOutputs = [ + JSON.parse(fs.readFileSync('/tmp/compile_output.json', 'utf8')), + JSON.parse(fs.readFileSync('/tmp/update_output.json', 'utf8')), + JSON.parse(fs.readFileSync('/tmp/run_once_output.json', 'utf8')), + JSON.parse(fs.readFileSync('/tmp/run_cached_output.json', 'utf8')) + ]; + const outputNames = ['compile', 'update', 'run_once', 'run_cached']; + const sectionTitles = ['Compilation', 'Update', 'Run Once', 'Cached Execution']; + + let hasSignificantChanges = false; + let regressionDetails = []; + + for (let i = 0; i < allOutputs.length; i++) { + const benchmarkOutput = allOutputs[i]; + const outputName = outputNames[i]; + const sectionTitle = sectionTitles[i]; + + for (const benchmark of benchmarkOutput) { + if (benchmark.profiles?.[0]?.summaries?.parts?.[0]?.metrics_summary?.Callgrind?.Ir?.diffs?.diff_pct) { + const diffPct = parseFloat(benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.diffs.diff_pct); + const oldValue = benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.metrics.Both[1].Int; + const newValue = benchmark.profiles[0].summaries.parts[0].metrics_summary.Callgrind.Ir.metrics.Both[0].Int; + const absoluteChange = Math.abs(newValue - oldValue); + + if (isSignificantChange(diffPct, absoluteChange, outputName)) { + hasSignificantChanges = true; + regressionDetails.push({ + module_path: benchmark.module_path, + id: benchmark.id, + diffPct, + absoluteChange, + sectionTitle + }); + } + } + } + } + + core.setOutput('has-significant-changes', hasSignificantChanges); + core.setOutput('regression-details', JSON.stringify(regressionDetails)); + - name: Comment PR + if: github.event.pull_request.head.repo.full_name == github.repository uses: actions/github-script@v7 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | - const compileOutput = JSON.parse(`${{ steps.benchmark.outputs.COMPILE_OUTPUT }}`); - const updateOutput = JSON.parse(`${{ steps.benchmark.outputs.UPDATE_OUTPUT }}`); - const runOnceOutput = JSON.parse(`${{ steps.benchmark.outputs.RUN_ONCE_OUTPUT }}`); - const runCachedOutput = JSON.parse(`${{ steps.benchmark.outputs.RUN_CACHED_OUTPUT }}`); - - let significantChanges = false; + const fs = require('fs'); + + const compileOutput = JSON.parse(fs.readFileSync('/tmp/compile_output.json', 'utf8')); + const updateOutput = JSON.parse(fs.readFileSync('/tmp/update_output.json', 'utf8')); + const runOnceOutput = JSON.parse(fs.readFileSync('/tmp/run_once_output.json', 'utf8')); + const runCachedOutput = JSON.parse(fs.readFileSync('/tmp/run_cached_output.json', 'utf8')); + + const hasSignificantChanges = '${{ steps.analyze.outputs.has-significant-changes }}' === 'true'; let commentBody = ""; function formatNumber(num) { @@ -160,13 +220,31 @@ jobs: let sectionBody = ""; let hasResults = false; let hasSignificantChanges = false; + + function isSignificantChange(diffPct, absoluteChange, benchmarkType) { + const meetsPercentageThreshold = Math.abs(diffPct) > 5; + const meetsAbsoluteThreshold = absoluteChange > 200000; + const isCachedExecution = benchmarkType === 'run_cached' || + benchmarkType.includes('Cached Execution'); + + return isCachedExecution + ? (meetsPercentageThreshold && meetsAbsoluteThreshold) + : meetsPercentageThreshold; + } for (const benchmark of benchmarkOutput) { - if (benchmark.callgrind_summary && benchmark.callgrind_summary.summaries) { - const summary = benchmark.callgrind_summary.summaries[0]; - const irDiff = summary.events.Ir; - - if (irDiff.diff_pct !== null) { + if (benchmark.profiles && benchmark.profiles.length > 0) { + const profile = benchmark.profiles[0]; + if (profile.summaries && profile.summaries.parts && profile.summaries.parts.length > 0) { + const part = profile.summaries.parts[0]; + if (part.metrics_summary && part.metrics_summary.Callgrind && part.metrics_summary.Callgrind.Ir) { + const irData = part.metrics_summary.Callgrind.Ir; + if (irData.diffs && irData.diffs.diff_pct !== null) { + const irDiff = { + diff_pct: parseFloat(irData.diffs.diff_pct), + old: irData.metrics.Both[1].Int, + new: irData.metrics.Both[0].Int + }; hasResults = true; const changePercentage = formatPercentage(irDiff.diff_pct); const color = irDiff.diff_pct > 0 ? "red" : "lime"; @@ -178,19 +256,23 @@ jobs: sectionBody += "
\nDetailed metrics\n\n```\n"; sectionBody += `Baselines: master| HEAD\n`; - for (const [eventKind, costsDiff] of Object.entries(summary.events)) { - if (costsDiff.diff_pct !== null) { - const changePercentage = formatPercentage(costsDiff.diff_pct); - const line = `${padRight(eventKind, 20)} ${padLeft(formatNumber(costsDiff.old), 11)}|${padLeft(formatNumber(costsDiff.new), 11)} ${padLeft(changePercentage, 15)}`; + for (const [metricName, metricData] of Object.entries(part.metrics_summary.Callgrind)) { + if (metricData.diffs && metricData.diffs.diff_pct !== null) { + const changePercentage = formatPercentage(parseFloat(metricData.diffs.diff_pct)); + const oldValue = metricData.metrics.Both[1].Int || metricData.metrics.Both[1].Float; + const newValue = metricData.metrics.Both[0].Int || metricData.metrics.Both[0].Float; + const line = `${padRight(metricName, 20)} ${padLeft(formatNumber(Math.round(oldValue)), 11)}|${padLeft(formatNumber(Math.round(newValue)), 11)} ${padLeft(changePercentage, 15)}`; sectionBody += `${line}\n`; } } sectionBody += "```\n
\n\n"; - - if (Math.abs(irDiff.diff_pct) > 5) { - significantChanges = true; - hasSignificantChanges = true; + + if (isSignificantChange(irDiff.diff_pct, Math.abs(irDiff.new - irDiff.old), sectionTitle)) { + significantChanges = true; + hasSignificantChanges = true; + } + } } } } @@ -236,7 +318,7 @@ jobs: if (commentBody.length > 0) { const output = `
\nPerformance Benchmark Results\n\n${commentBody}\n
`; - if (significantChanges) { + if (hasSignificantChanges) { github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, @@ -250,3 +332,13 @@ jobs: } else { console.log("No benchmark results to display."); } + + - name: Fail on significant regressions + if: steps.analyze.outputs.has-significant-changes == 'true' + uses: actions/github-script@v7 + with: + script: | + const regressionDetails = JSON.parse('${{ steps.analyze.outputs.regression-details }}'); + const firstRegression = regressionDetails[0]; + + core.setFailed(`Significant performance regression detected: ${firstRegression.module_path} ${firstRegression.id} increased by ${firstRegression.absoluteChange.toLocaleString()} instructions (${firstRegression.diffPct.toFixed(2)}%)`); diff --git a/.github/workflows/library-rawkit.yml b/.github/workflows/library-rawkit.yml index b7b4bb3153..569d596500 100644 --- a/.github/workflows/library-rawkit.yml +++ b/.github/workflows/library-rawkit.yml @@ -38,6 +38,13 @@ jobs: - name: ๐Ÿ“ฆ Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.6 + continue-on-error: true + + - name: ๐Ÿ”ง Fallback if sccache fails + if: failure() + run: | + echo "sccache failed, disabling it" + echo "RUSTC_WRAPPER=" >> $GITHUB_ENV - name: ๐Ÿ”ฌ Check Rust formatting run: | @@ -56,4 +63,4 @@ jobs: - name: ๐Ÿ“ˆ Run sccache stat for check shell: bash - run: sccache --show-stats + run: sccache --show-stats || echo "sccache stats unavailable" diff --git a/.github/workflows/provide-shaders.yml b/.github/workflows/provide-shaders.yml new file mode 100644 index 0000000000..25ae724691 --- /dev/null +++ b/.github/workflows/provide-shaders.yml @@ -0,0 +1,31 @@ +name: Provide Shaders + +on: + push: + branches: + - master + workflow_dispatch: {} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main + + - name: Build graphene raster nodes shaders + run: nix build .nix#raster-nodes-shaders && cp result raster_nodes_shaders_entrypoint.wgsl + + - name: Upload graphene raster nodes shaders to artifacts repository + run: | + bash .github/workflows/scripts/artifact-upload.bash \ + ${{ vars.ARTIFACTS_REPO_OWNER }} \ + ${{ vars.ARTIFACTS_REPO_NAME }} \ + ${{ vars.ARTIFACTS_REPO_BRANCH }} \ + rev/${{ github.sha }}/raster_nodes_shaders_entrypoint.wgsl \ + raster_nodes_shaders_entrypoint.wgsl \ + "${{ github.sha }} raster_nodes_shaders_entrypoint.wgsl" \ + ${{ secrets.ARTIFACTS_REPO_TOKEN }} diff --git a/.github/workflows/scripts/artifact-upload.bash b/.github/workflows/scripts/artifact-upload.bash new file mode 100644 index 0000000000..5e8f50dec0 --- /dev/null +++ b/.github/workflows/scripts/artifact-upload.bash @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat < + +Arguments: + owner : GitHub user or organization of the target repo + repo : Target repo name + branch : Branch name (e.g. main) + target-path : Full path (including folders + filename) in the target repo where to upload + artifact-file : Local file path to upload + commit-message : Commit message for creating/updating the file + github-token : GitHub token (PAT or equivalent) with write access to the target repo + +This will perform a GitHub API PUT to /repos/{owner}/{repo}/contents/{target-path}. +If a file already exists at that path, it will auto-detect the SHA and update; otherwise it will create a new one. +EOF + exit 1 +} + +if [ $# -ne 7 ]; then + usage +fi + +OWNER="$1" +REPO="$2" +BRANCH="$3" +TARGET_PATH="$4" +ARTIFACT_PATH="$5" +COMMIT_MSG="$6" +TOKEN="$7" + +if [ ! -f "$ARTIFACT_PATH" ]; then + echo "Error: artifact file not found: $ARTIFACT_PATH" >&2 + exit 1 +fi + +LOCAL_SHA=$(git hash-object "$ARTIFACT_PATH") +echo "Local blob SHA: $LOCAL_SHA" + +GET_URL="https://api.github.com/repos/${OWNER}/${REPO}/contents/${TARGET_PATH}?ref=${BRANCH}" +GET_RESPONSE=$(curl -s -H "Authorization: token ${TOKEN}" "$GET_URL") + +REMOTE_SHA=$(echo "$GET_RESPONSE" | jq -r .sha 2>/dev/null || echo "") + +if [ "$REMOTE_SHA" != "null" ] && [ -n "$REMOTE_SHA" ]; then + echo "Remote blob SHA: $REMOTE_SHA" + if [ "$LOCAL_SHA" = "$REMOTE_SHA" ]; then + echo "The remote file is identical. Skipping upload." + exit 0 + else + echo "Remote file differs. Preparing to upload." + fi +else + echo "No existing remote file or no SHA found. Creating." +fi + +CONTENT_TMP_BASE64=$(mktemp) +if base64 --help 2>&1 | grep -q -- "-w"; then + base64 -w 0 "$ARTIFACT_PATH" > "$CONTENT_TMP_BASE64" +else + base64 "$ARTIFACT_PATH" | tr -d '\n' > "$CONTENT_TMP_BASE64" +fi + +PAYLOAD_TMP=$(mktemp) +jq -n \ + --arg message "$COMMIT_MSG" \ + --arg branch "$BRANCH" \ + --arg sha "$REMOTE_SHA" \ + --rawfile content "$CONTENT_TMP_BASE64" \ + '{ + message: $message, + content: $content, + branch: $branch + } + (if ($sha != "" and $sha != "null") then { sha: $sha } else {} end)' \ + > "$PAYLOAD_TMP" + +UPLOAD_RESPONSE=$(curl -s -X PUT \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d @"$PAYLOAD_TMP" \ + "https://api.github.com/repos/${OWNER}/${REPO}/contents/${TARGET_PATH}") + +echo "Upload Response:" +echo "$UPLOAD_RESPONSE" + +rm -f "$CONTENT_TMP_BASE64" "$PAYLOAD_TMP" diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 58559c4311..3b8fb54eeb 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -12,7 +12,7 @@ on: workflow_dispatch: {} env: CARGO_TERM_COLOR: always - INDEX_HTML_HEAD_INCLUSION: + INDEX_HTML_HEAD_INCLUSION: jobs: build: @@ -26,10 +26,18 @@ jobs: - name: ๐Ÿ“ฅ Clone and checkout repository uses: actions/checkout@v3 + # We can remove this step once `ubuntu-latest` has Node.js 22 or newer for its native TypeScript support. See: + # https://github.com/actions/runner-images?tab=readme-ov-file#available-images + # https://nodejs.org/en/learn/typescript/run-natively + - name: ๐Ÿ“ฆ Install the latest Node.js + uses: actions/setup-node@v4 + with: + node-version: "latest" + - name: ๐Ÿ•ธ Install Zola uses: taiki-e/install-action@v2 with: - tool: zola@0.20.0 + tool: zola@0.22.0 - name: ๐Ÿ” Check if `website/other` directory changed uses: dorny/paths-filter@v3 @@ -59,25 +67,34 @@ jobs: rustup update stable echo "๐Ÿฆ€ Latest updated version of Rust:" rustc --version - cargo test --package graphite-editor --lib -- messages::message::test::generate_message_tree + cd tools/editor-message-tree + cargo run + cd ../.. mkdir artifacts - mv hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt + mv website/generated/hierarchical_message_system_tree.txt artifacts/hierarchical_message_system_tree.txt - - name: ๐Ÿšš Move `artifacts` contents to the project root + - name: ๐Ÿšš Move `artifacts` contents to website/generated run: | - mv artifacts/* . + mkdir -p website/generated + mv artifacts/* website/generated/ - name: ๐Ÿ”ง Build auto-generated code docs artifacts into HTML run: | cd website npm run generate-editor-structure + - name: ๐Ÿ“ƒ Generate node catalog documentation + run: | + cd tools/node-docs + cargo run + - name: ๐ŸŒ Build Graphite website with Zola env: MODE: prod run: | cd website - npm run install-fonts + npm ci + npm run lint zola --config config.toml build --minify - name: ๐Ÿ“ค Publish to Cloudflare Pages diff --git a/.gitignore b/.gitignore index 29af1eed11..d6cc8a574c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ +branding/ target/ +result/ +.flatpak-builder/ *.spv *.exrc perf.data* @@ -7,5 +10,4 @@ profile.json.gz flamegraph.svg .idea/ .direnv -hierarchical_message_system_tree.txt -hierarchical_message_system_tree.html +.DS_Store diff --git a/.nix/deps/cef.nix b/.nix/deps/cef.nix new file mode 100644 index 0000000000..c64a1a1c7e --- /dev/null +++ b/.nix/deps/cef.nix @@ -0,0 +1,28 @@ +{ pkgs, inputs, ... }: + +let + cef = pkgs.cef-binary.overrideAttrs { + postInstall = '' + strip $out/Release/*.so* + ''; + }; + + cefPath = pkgs.runCommand "cef-path" { } '' + mkdir -p $out + + ln -s ${cef}/include $out/include + find ${cef}/Release -name "*" -type f -exec ln -s {} $out/ \; + find ${cef}/Resources -name "*" -maxdepth 1 -exec ln -s {} $out/ \; + + echo '${ + builtins.toJSON { + type = "minimal"; + name = builtins.baseNameOf cef.src.url; + sha1 = ""; + } + }' > $out/archive.json + ''; +in +{ + env.CEF_PATH = cefPath; +} diff --git a/.nix/deps/crane.nix b/.nix/deps/crane.nix new file mode 100644 index 0000000000..368827082c --- /dev/null +++ b/.nix/deps/crane.nix @@ -0,0 +1,5 @@ +{ pkgs, inputs, ... }: + +{ + lib = inputs.crane.mkLib pkgs; +} diff --git a/.nix/deps/rust-gpu.nix b/.nix/deps/rust-gpu.nix new file mode 100644 index 0000000000..2970d515a2 --- /dev/null +++ b/.nix/deps/rust-gpu.nix @@ -0,0 +1,58 @@ +{ pkgs, inputs, ... }: + +let + extensions = [ + "rust-src" + "rust-analyzer" + "clippy" + "cargo" + "rustc-dev" + "llvm-tools" + ]; + toolchain = pkgs.rust-bin.nightly."2025-06-23".default.override { + inherit extensions; + }; + cargo = pkgs.writeShellScriptBin "cargo" '' + #!${pkgs.lib.getExe pkgs.bash} + + filtered_args=() + for arg in "$@"; do + case "$arg" in + +nightly|+nightly-*) ;; + *) filtered_args+=("$arg") ;; + esac + done + + exec ${toolchain}/bin/cargo ${"\${filtered_args[@]}"} + ''; + rustc_codegen_spirv = + (pkgs.makeRustPlatform { + cargo = toolchain; + rustc = toolchain; + }).buildRustPackage + (finalAttrs: { + pname = "rustc_codegen_spirv"; + version = "0-unstable-2025-08-04"; + src = pkgs.fetchFromGitHub { + owner = "Firestar99"; + repo = "rust-gpu-new"; + rev = "c12f216121820580731440ee79ebc7403d6ea04f"; + hash = "sha256-rG1cZvOV0vYb1dETOzzbJ0asYdE039UZImobXZfKIno="; + }; + cargoHash = "sha256-AEigcEc5wiBd3zLqWN/2HSbkfOVFneAqNvg9HsouZf4="; + cargoBuildFlags = [ + "-p" + "rustc_codegen_spirv" + "--features=use-compiled-tools" + "--no-default-features" + ]; + doCheck = false; + }); +in +{ + toolchain = toolchain; + env = { + RUST_GPU_PATH_OVERRIDE = "${cargo}/bin:${toolchain}/bin"; + RUSTC_CODEGEN_SPIRV_PATH = "${rustc_codegen_spirv}/lib/librustc_codegen_spirv.so"; + }; +} diff --git a/.nix/dev.nix b/.nix/dev.nix new file mode 100644 index 0000000000..d80d7793e6 --- /dev/null +++ b/.nix/dev.nix @@ -0,0 +1,22 @@ +{ + pkgs, + deps, + libs, + tools, + ... +}: + +pkgs.mkShell ( + { + packages = tools.all ++ libs.all; + + LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath libs.all}:${deps.cef.env.CEF_PATH}"; + XDG_DATA_DIRS = "${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS"; + + shellHook = '' + alias cargo='mold --run cargo' + ''; + } + // deps.cef.env + // deps.rustGPU.env +) diff --git a/.nix/flake.lock b/.nix/flake.lock index dc9b853784..2d6d42777a 100644 --- a/.nix/flake.lock +++ b/.nix/flake.lock @@ -1,5 +1,20 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1763938834, + "narHash": "sha256-j8iB0Yr4zAvQLueCZ5abxfk6fnG/SJ5JnGUziETjwfg=", + "owner": "ipetkov", + "repo": "crane", + "rev": "d9e753122e51cee64eb8d2dddfe11148f339f5a2", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "flake-compat": { "locked": { "lastModified": 1733328505, @@ -34,11 +49,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1754214453, - "narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=", + "lastModified": 1764242076, + "narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376", + "rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4", "type": "github" }, "original": { @@ -50,6 +65,7 @@ }, "root": { "inputs": { + "crane": "crane", "flake-compat": "flake-compat", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", @@ -63,11 +79,11 @@ ] }, "locked": { - "lastModified": 1753238793, - "narHash": "sha256-jmQeEpgX+++MEgrcikcwoSiI7vDZWLP0gci7XiWb9uQ=", + "lastModified": 1764297505, + "narHash": "sha256-qrLpVu2/hA9Cu6IovMEsgh9YRyvmmWS+bSx7C1JGChA=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "0ad7ab4ca8e83febf147197e65c006dff60623ab", + "rev": "9623580f8ce09ec444b9aca107566ec5db110e62", "type": "github" }, "original": { diff --git a/.nix/flake.nix b/.nix/flake.nix index ba9bd62714..5a1f419347 100644 --- a/.nix/flake.nix +++ b/.nix/flake.nix @@ -12,8 +12,6 @@ # - Development shell: `nix develop .nix` from the project root # - Run in dev shell with direnv: add `use flake` to .envrc { - description = "Development environment and build configuration"; - inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; rust-overlay = { @@ -21,108 +19,151 @@ inputs.nixpkgs.follows = "nixpkgs"; }; flake-utils.url = "github:numtide/flake-utils"; + 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 = { nixpkgs, rust-overlay, flake-utils, ... }: - flake-utils.lib.eachDefaultSystem (system: + outputs = + inputs: + inputs.flake-utils.lib.eachDefaultSystem ( + system: let - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { - inherit system overlays; + info = { + pname = "graphite"; + version = "unstable"; + src = pkgs.lib.cleanSourceWith { + src = ./..; + filter = path: type: !(type == "directory" && builtins.baseNameOf path == ".nix"); + }; }; - rustc-wasm = pkgs.rust-bin.stable.latest.default.override { - targets = [ "wasm32-unknown-unknown" ]; - extensions = [ "rust-src" "rust-analyzer" "clippy" "cargo" ]; + pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ (import inputs.rust-overlay) ]; }; - libcef = pkgs.libcef.overrideAttrs (finalAttrs: previousAttrs: { - version = "139.0.17"; - gitRevision = "6c347eb"; - chromiumVersion = "139.0.7258.31"; - srcHash = "sha256-kRMO8DP4El1qytDsAZBdHvR9AAHXce90nPdyfJailBg="; + deps = { + crane = import ./deps/crane.nix { inherit pkgs inputs; }; + cef = import ./deps/cef.nix { inherit pkgs inputs; }; + rustGPU = import ./deps/rust-gpu.nix { inherit pkgs inputs; }; + }; - __intentionallyOverridingVersion = true; + libs = rec { + desktop = [ + pkgs.wayland + pkgs.openssl + pkgs.vulkan-loader + pkgs.libraw + pkgs.libGL + ]; + desktop-x11 = [ + pkgs.libxkbcommon + pkgs.xorg.libXcursor + pkgs.xorg.libxcb + pkgs.xorg.libX11 + ]; + desktop-all = desktop ++ desktop-x11; + all = desktop-all; + }; - postInstall = '' - strip $out/lib/* - ''; - }); + tools = rec { + desktop = [ + pkgs.pkg-config + ]; + frontend = [ + pkgs.lld + pkgs.nodejs + pkgs.nodePackages.npm + pkgs.binaryen + pkgs.wasm-bindgen-cli_0_2_100 + pkgs.wasm-pack + pkgs.cargo-about + ]; + dev = [ + pkgs.rustc + pkgs.cargo + pkgs.rust-analyzer + pkgs.clippy + pkgs.rustfmt - libcefPath = pkgs.runCommand "libcef-path" {} '' - mkdir -p $out + pkgs.git - ln -s ${libcef}/include $out/include - find ${libcef}/lib -type f -name "*" -exec ln -s {} $out/ \; - find ${libcef}/libexec -type f -name "*" -exec ln -s {} $out/ \; - cp -r ${libcef}/share/cef/* $out/ + pkgs.cargo-watch + pkgs.cargo-nextest + pkgs.cargo-expand - echo '${builtins.toJSON { - type = "minimal"; - name = builtins.baseNameOf libcef.src.url; - sha1 = ""; - }}' > $out/archive.json - ''; + # Linker + pkgs.mold - # Shared build inputs - system libraries that need to be in LD_LIBRARY_PATH - buildInputs = with pkgs; [ - # System libraries - wayland - openssl - vulkan-loader - libraw - libGL + # Profiling tools + pkgs.gnuplot + pkgs.samply + pkgs.cargo-flamegraph - # X11 libraries, not needed on wayland! Remove when x11 is finally dead - libxkbcommon - xorg.libXcursor - xorg.libxcb - xorg.libX11 - ]; - - # Development tools that don't need to be in LD_LIBRARY_PATH - buildTools = [ - rustc-wasm - pkgs.nodejs - pkgs.nodePackages.npm - pkgs.binaryen - pkgs.wasm-bindgen-cli - pkgs.wasm-pack - pkgs.pkg-config - pkgs.git - pkgs.cargo-about - - # Linker - pkgs.mold - ]; - # Development tools that don't need to be in LD_LIBRARY_PATH - devTools = with pkgs; [ - cargo-watch - cargo-nextest - cargo-expand - - # Profiling tools - gnuplot - samply - cargo-flamegraph - ]; + # Plotting tools + pkgs.graphviz + ]; + all = desktop ++ frontend ++ dev; + }; in { - # Development shell configuration - devShells.default = pkgs.mkShell { - packages = buildInputs ++ buildTools ++ devTools; + packages = rec { + graphiteWithArgs = + args: + (import ./pkgs/graphite.nix { + pkgs = pkgs // { + inherit raster-nodes-shaders; + }; + inherit + info + inputs + deps + libs + tools + ; + }) + args; + graphite = graphiteWithArgs { }; + graphite-dev = graphiteWithArgs { dev = true; }; + graphite-without-resources = graphiteWithArgs { embeddedResources = false; }; + graphite-without-resources-dev = graphiteWithArgs { + embeddedResources = false; + dev = true; + }; + graphite-bundle = import ./pkgs/graphite-bundle.nix { + inherit pkgs graphite; + }; + graphite-flatpak-manifest = import ./pkgs/graphite-flatpak-manifest.nix { + inherit pkgs; + archive = graphite-bundle.tar; + }; + #TODO: graphene-cli = import ./pkgs/graphene-cli.nix { inherit info pkgs inputs deps libs tools; }; + raster-nodes-shaders = import ./pkgs/raster-nodes-shaders.nix { + inherit + info + pkgs + inputs + deps + libs + tools + ; + }; - LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath buildInputs}:${libcefPath}"; - CEF_PATH = libcefPath; - XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS"; - - shellHook = '' - alias cargo='mold --run cargo' - ''; + default = graphite; }; + + devShells.default = import ./dev.nix { + inherit + pkgs + deps + libs + tools + ; + }; + + formatter = pkgs.nixfmt-tree; } ); } diff --git a/.nix/pkgs/graphite-bundle.nix b/.nix/pkgs/graphite-bundle.nix new file mode 100644 index 0000000000..f6e685d65d --- /dev/null +++ b/.nix/pkgs/graphite-bundle.nix @@ -0,0 +1,91 @@ +{ + pkgs, + graphite, +}: +let + bundle = + { + pkgs, + graphite, + archive ? false, + compression ? null, + passthru ? {}, + }: + ( + let + tar = if compression == null then archive else true; + nameArchiveSuffix = if tar then ".tar" else ""; + nameCompressionSuffix = if compression == null then "" else "." + compression; + name = "graphite-bundle${nameArchiveSuffix}${nameCompressionSuffix}"; + build = '' + mkdir -p out + mkdir -p out/bin + cp ${graphite}/bin/.graphite-wrapped out/bin/graphite + chmod -v +w out/bin/graphite + patchelf --set-rpath '$ORIGIN/../lib:$ORIGIN/../lib/cef' --set-interpreter '/lib64/ld-linux-x86-64.so.2' out/bin/graphite + mkdir -p out/lib/cef + mkdir -p ./cef + tar -xvf ${pkgs.cef-binary.src} -C ./cef --strip-components=1 + cp -r ./cef/Release/* out/lib/cef/ + cp -r ./cef/Resources/* out/lib/cef/ + find "out/lib/cef/locales" -type f ! -name 'en-US*' -delete + ${pkgs.bintools}/bin/strip out/lib/cef/*.so* + cp -r ${graphite}/share out/share + ''; + install = + if tar then + '' + cd out + tar -c \ + --sort=name \ + --mtime='@1' --clamp-mtime \ + --owner=0 --group=0 --numeric-owner \ + --mode='u=rwX,go=rX' \ + --format=posix \ + --pax-option=delete=atime,delete=ctime \ + --no-acls --no-xattrs --no-selinux \ + * ${ + if compression == "xz" then + "| xz " + else if compression == "gz" then + "| gzip -n " + else + "" + }> $out + '' + else + '' + mkdir -p $out + cp -r out/* $out/ + ''; + in + + pkgs.runCommand name + { + inherit passthru; + } + '' + ${build} + ${install} + '' + ); +in +bundle { + inherit pkgs graphite; + passthru = { + tar = bundle { + inherit pkgs graphite; + archive = true; + passthru = { + gz = bundle { + inherit pkgs graphite; + compression = "gz"; + }; + xz = bundle { + inherit pkgs graphite; + compression = "xz"; + }; + }; + }; + }; +} diff --git a/.nix/pkgs/graphite-flatpak-manifest.nix b/.nix/pkgs/graphite-flatpak-manifest.nix new file mode 100644 index 0000000000..03d7146f74 --- /dev/null +++ b/.nix/pkgs/graphite-flatpak-manifest.nix @@ -0,0 +1,37 @@ +{ + pkgs, + archive, +}: + +(pkgs.formats.json { }).generate "art.graphite.Graphite.json" { + app-id = "art.graphite.Graphite"; + runtime = "org.freedesktop.Platform"; + runtime-version = "25.08"; + sdk = "org.freedesktop.Sdk"; + command = "graphite"; + finish-args = [ + "--device=dri" + "--share=ipc" + "--socket=wayland" + "--socket=fallback-x11" + "--share=network" + ]; + modules = [ + { + name = "app"; + buildsystem = "simple"; + build-commands = [ + "mkdir -p /app" + "cp -r ./* /app/" + "chmod +x /app/bin/*" + ]; + sources = [ + { + type = "archive"; + path = archive; + strip-components = 0; + } + ]; + } + ]; +} diff --git a/.nix/pkgs/graphite.nix b/.nix/pkgs/graphite.nix new file mode 100644 index 0000000000..1517869bc4 --- /dev/null +++ b/.nix/pkgs/graphite.nix @@ -0,0 +1,146 @@ +{ + info, + pkgs, + inputs, + deps, + libs, + tools, + ... +}: + +{ + embeddedResources ? true, + dev ? false, +}: + +let + brandingTar = pkgs.fetchurl ( + let + lockContent = builtins.readFile "${info.src}/.branding"; + lines = builtins.filter (s: s != [ ]) (builtins.split "\n" lockContent); + url = builtins.elemAt lines 0; + hash = builtins.elemAt lines 1; + in + { + url = url; + sha256 = hash; + } + ); + branding = pkgs.runCommand "${info.pname}-branding" { } '' + mkdir -p $out + tar -xvf ${brandingTar} -C $out --strip-components 1 + ''; + resourcesCommon = { + pname = "${info.pname}-resources"; + inherit (info) version src; + strictDeps = true; + doCheck = false; + nativeBuildInputs = tools.frontend; + env.CARGO_PROFILE = if dev then "dev" else "release"; + cargoExtraArgs = "--target wasm32-unknown-unknown -p graphite-wasm --no-default-features --features native"; + }; + resources = deps.crane.lib.buildPackage ( + resourcesCommon + // { + cargoArtifacts = deps.crane.lib.buildDepsOnly resourcesCommon; + + npmDeps = pkgs.importNpmLock { + npmRoot = "${info.src}/frontend"; + }; + + npmRoot = "frontend"; + npmConfigScript = "setup"; + makeCacheWritable = true; + + nativeBuildInputs = tools.frontend ++ [ pkgs.importNpmLock.npmConfigHook ]; + + prePatch = '' + mkdir branding + cp -r ${branding}/* branding + cp ${info.src}/.branding branding/.branding + ''; + + buildPhase = '' + export HOME="$TMPDIR" + + pushd frontend + npm run native:build-${if dev then "dev" else "production"} + popd + ''; + + installPhase = '' + mkdir -p $out + cp -r frontend/dist/* $out/ + ''; + } + ); + common = { + inherit (info) pname version src; + strictDeps = true; + buildInputs = libs.desktop-all; + nativeBuildInputs = tools.desktop ++ [ pkgs.makeWrapper ]; + env = deps.cef.env // { + CARGO_PROFILE = if dev then "dev" else "release"; + }; + cargoExtraArgs = "-p graphite-desktop${ + if embeddedResources then "" else " --no-default-features --features recommended" + }"; + doCheck = false; + }; +in + +deps.crane.lib.buildPackage ( + common + // { + cargoArtifacts = deps.crane.lib.buildDepsOnly common; + + env = + common.env + // { + RASTER_NODES_SHADER_PATH = pkgs.raster-nodes-shaders; + } + // ( + if embeddedResources then + { + EMBEDDED_RESOURCES = resources; + } + else + { } + ) // { + GRAPHITE_GIT_COMMIT_HASH = inputs.self.rev or "unknown"; + GRAPHITE_GIT_COMMIT_DATE = inputs.self.lastModified or "unknown"; + }; + + postUnpack = '' + mkdir ./branding + cp -r ${branding}/* ./branding + ''; + + preBuild = if inputs.self ? rev then '' + export GRAPHITE_GIT_COMMIT_DATE="$(date -u -d "@$GRAPHITE_GIT_COMMIT_DATE" +"%Y-%m-%dT%H:%M:%SZ")" + '' else ""; + + installPhase = '' + mkdir -p $out/bin + cp target/${if dev then "debug" else "release"}/graphite $out/bin/graphite + + mkdir -p $out/share/applications + cp $src/desktop/assets/*.desktop $out/share/applications/ + + mkdir -p $out/share/icons/hicolor/scalable/apps + cp ${branding}/app-icons/graphite.svg $out/share/icons/hicolor/scalable/apps/art.graphite.Graphite.svg + mkdir -p $out/share/icons/hicolor/512x512/apps + cp ${branding}/app-icons/graphite-512.png $out/share/icons/hicolor/512x512/apps/art.graphite.Graphite.png + mkdir -p $out/share/icons/hicolor/256x256/apps + cp ${branding}/app-icons/graphite-256.png $out/share/icons/hicolor/256x256/apps/art.graphite.Graphite.png + mkdir -p $out/share/icons/hicolor/128x128/apps + cp ${branding}/app-icons/graphite-128.png $out/share/icons/hicolor/128x128/apps/art.graphite.Graphite.png + ''; + + postFixup = '' + wrapProgram "$out/bin/graphite" \ + --prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath libs.desktop-all}:${deps.cef.env.CEF_PATH}" \ + --set CEF_PATH "${deps.cef.env.CEF_PATH}" + ''; + } +) diff --git a/.nix/pkgs/raster-nodes-shaders.nix b/.nix/pkgs/raster-nodes-shaders.nix new file mode 100644 index 0000000000..46e2f90a29 --- /dev/null +++ b/.nix/pkgs/raster-nodes-shaders.nix @@ -0,0 +1,36 @@ +{ + info, + pkgs, + inputs, + deps, + libs, + tools, + ... +}: + +(deps.crane.lib.overrideToolchain (_: deps.rustGPU.toolchain)).buildPackage { + pname = "raster-nodes-shaders"; + inherit (info) version src; + + cargoVendorDir = deps.crane.lib.vendorMultipleCargoDeps { + inherit (deps.crane.lib.findCargoFiles (deps.crane.lib.cleanCargoSource info.src)) cargoConfigs; + cargoLockList = [ + "${info.src}/Cargo.lock" + "${deps.rustGPU.toolchain.passthru.availableComponents.rust-src}/lib/rustlib/src/rust/library/Cargo.lock" + ]; + }; + + strictDeps = true; + + env = deps.rustGPU.env; + + buildPhase = '' + cargo build -r -p raster-nodes-shaders + ''; + + installPhase = '' + cp target/spirv-builder/spirv-unknown-naga-wgsl/release/deps/raster_nodes_shaders_entrypoint.wgsl $out + ''; + + doCheck = false; +} diff --git a/.nix/shell.nix b/.nix/shell.nix index 17dae9531b..5b13af2e20 100644 --- a/.nix/shell.nix +++ b/.nix/shell.nix @@ -16,16 +16,13 @@ # > 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 +(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/.vscode/extensions.json b/.vscode/extensions.json index e37a0efd11..dd8c135ef9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -11,10 +11,10 @@ // Code quality "wayou.vscode-todo-highlight", "streetsidesoftware.code-spell-checker", - // Helpful + // Git "mhutchie.git-graph", - "waderyan.gitblame", "qezhu.gitlink", + // Helpful "wmaurer.change-case" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 1a8ced3869..5fbf8c7f1b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,13 +26,22 @@ // Configured in `.prettierrc` "editor.defaultFormatter": "esbenp.prettier-vscode" }, + // Website: don't format Zola/Tera-templated HTML on save + "[html]": { + "editor.formatOnSave": false + }, // Handlebars: don't save on format // (`about.hbs` is used by Cargo About to encode license information) "[handlebars]": { "editor.formatOnSave": false }, // Rust Analyzer config + "rust-analyzer.check.command": "clippy", "rust-analyzer.cargo.allTargets": false, + "rust-analyzer.procMacro.ignored": { + "serde_derive": ["Serialize", "Deserialize"], + "specta_macros": ["Type"] // Disabled because of: https://github.com/specta-rs/specta/issues/387 + }, // ESLint config "eslint.format.enable": true, "eslint.workingDirectories": ["./frontend", "./website"], @@ -43,13 +52,35 @@ "vite-plugin-svelte-css-no-scopable-elements": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` "a11y-no-static-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` "a11y-no-noninteractive-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` - "a11y-click-events-have-key-events": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` + "a11y-click-events-have-key-events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` + "a11y_consider_explicit_label": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` + "a11y_click_events_have_key_events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` + "a11y_no_noninteractive_element_interactions": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` }, + // Git Graph config + "git-graph.repository.fetchAndPrune": true, + "git-graph.repository.showRemoteHeads": false, + "git-graph.repository.commits.fetchAvatars": true, + // VS Code Git config + "git.autofetch": true, + "git.enableStatusBarSync": false, + "git.showActionButton": { + "sync": false + }, + // CSpell config + "cSpell.language": "en-US", + "cSpell.logLevel": "Information", + "cSpell.allowCompoundWords": true, + // Other extensions config + "evenBetterToml.formatter.alignComments": false, + "package-json-upgrade.ignorePatterns": ["source-sans-pro"], // VS Code config "html.format.wrapLineLength": 200, "files.eol": "\n", "files.insertFinalNewline": true, "files.associations": { "*.graphite": "json" - } + }, + "editor.renderWhitespace": "boundary", + "editor.minimap.markSectionHeaderRegex": "// ===+\\n\\s*//\\s*(?