diff --git a/.branding b/.branding index 2c4bb8aab9..1e29c19dff 100644 --- a/.branding +++ b/.branding @@ -1,2 +1,2 @@ -https://github.com/Keavon/graphite-branded-assets/archive/f8b02e68c92f5bbd27626bdd7a51102303b70a40.tar.gz -d06fd7b79fa9b7509c23072fa56745415fdc6eb98575d15214b0acc47ea4dd42 +https://github.com/Keavon/graphite-branded-assets/archive/8ae15dc9c51a3855475d8cab1d0f29d9d9bc622c.tar.gz +c19abe4ac848f3c835e43dc065c59e20e60233ae023ea0a064c5fed442be2d3d diff --git a/.envrc b/.envrc index 930f30ba2a..3550a30f2d 100644 --- a/.envrc +++ b/.envrc @@ -1 +1 @@ -use flake .nix +use flake 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-linux-bundle.yml b/.github/workflows/build-linux-bundle.yml new file mode 100644 index 0000000000..d5584f3542 --- /dev/null +++ b/.github/workflows/build-linux-bundle.yml @@ -0,0 +1,75 @@ +name: Build Linux Bundle + +on: + push: + branches: + - master + workflow_dispatch: + inputs: + push_to_cache: + description: "Push to Nix Cache" + required: false + type: boolean + default: false + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + + - name: Free disk space + run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache + + - name: Build Nix Package + run: nix build --no-link --print-out-paths + + - name: Push to Nix Cache + if: github.ref == 'refs/heads/master' || inputs.push_to_cache == true + env: + NIX_CACHE_AUTH_TOKEN: ${{ secrets.NIX_CACHE_AUTH_TOKEN }} + run: | + nix run nixpkgs#cachix -- authtoken $NIX_CACHE_AUTH_TOKEN + nix build --no-link --print-out-paths | nix run nixpkgs#cachix -- push graphite + + - name: Build Linux Bundle + run: nix build .#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 .#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..b11eba7954 --- /dev/null +++ b/.github/workflows/build-mac-bundle.yml @@ -0,0 +1,157 @@ +name: Build Mac Bundle + +on: + workflow_dispatch: {} + 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: cargo 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 + if: github.ref == 'refs/heads/master' + 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 + if: github.ref == 'refs/heads/master' + 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 + if: github.ref == 'refs/heads/master' + 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 index 2da86b2d34..45669d0309 100644 --- a/.github/workflows/build-nix-package.yml +++ b/.github/workflows/build-nix-package.yml @@ -14,4 +14,4 @@ jobs: - uses: DeterminateSystems/magic-nix-cache-action@main - name: Build Nix Package Dev - run: nix build .nix#graphite-dev --print-build-logs + run: nix build .#graphite-dev --print-build-logs diff --git a/.github/workflows/build-production.yml b/.github/workflows/build-production.yml index 1be2e3e947..eae2aca9c6 100644 --- a/.github/workflows/build-production.yml +++ b/.github/workflows/build-production.yml @@ -52,9 +52,7 @@ jobs: - name: ๐ŸŒ Build Graphite web code env: NODE_ENV: production - run: | - cd frontend - mold -run npm run build + run: mold -run cargo run build web - name: ๐Ÿ“ค Publish to Cloudflare Pages id: cloudflare diff --git a/.github/workflows/build-win-bundle.yml b/.github/workflows/build-win-bundle.yml new file mode 100644 index 0000000000..720bdb63a7 --- /dev/null +++ b/.github/workflows/build-win-bundle.yml @@ -0,0 +1,173 @@ +name: Build Windows Bundle + +on: + workflow_dispatch: {} + 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 + shell: bash # `cargo-about` refuses to run in powershell + env: + CARGO_TERM_COLOR: always + run: cargo 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 + if: github.ref == 'refs/heads/master' + 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 + if: github.ref == 'refs/heads/master' + 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 + if: github.ref == 'refs/heads/master' + 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 + if: github.ref == 'refs/heads/master' + 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/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..4985abfe20 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,153 @@ +name: "CI" + +on: + pull_request: {} + merge_group: {} + +env: + CARGO_TERM_COLOR: always + +jobs: + # Rust format check on GitHub runner + rust-fmt: + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ“ฅ Clone and checkout repository + uses: actions/checkout@v3 + + - name: ๐Ÿšฆ Check if CI can be skipped + id: skip-check + uses: cariad-tech/merge-queue-ci-skipper@main + + - name: ๐Ÿฆ€ Install the latest Rust + if: steps.skip-check.outputs.skip-check != 'true' + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: ๐Ÿ”ฌ Check Rust formatting + if: steps.skip-check.outputs.skip-check != 'true' + run: cargo fmt --all -- --check + + # License compatibility check on GitHub runner + cargo-deny: + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ“ฅ Clone and checkout repository + uses: actions/checkout@v3 + + - name: ๐Ÿ“œ Check crate license compatibility for root workspace + uses: EmbarkStudios/cargo-deny-action@v2 + with: + command: check bans licenses sources + + - name: ๐Ÿ“œ Check crate license compatibility for /libraries/rawkit + uses: EmbarkStudios/cargo-deny-action@v2 + with: + command: check bans licenses sources + manifest-path: libraries/rawkit/Cargo.toml + + # Build the web app on the self-hosted wasm runner + build: + runs-on: [self-hosted, target/wasm] + permissions: + contents: write + deployments: write + pull-requests: write + actions: write + env: + RUSTC_WRAPPER: /usr/bin/sccache + CARGO_INCREMENTAL: 0 + SCCACHE_DIR: /var/lib/github-actions/.cache + + steps: + - name: ๐Ÿ“ฅ Clone and checkout repository + uses: actions/checkout@v3 + + - name: ๐Ÿšฆ Check if CI can be skipped + id: skip-check + uses: cariad-tech/merge-queue-ci-skipper@main + + - name: ๐Ÿ—‘ Clear wasm-bindgen cache + if: steps.skip-check.outputs.skip-check != 'true' + run: rm -r ~/.cache/.wasm-pack || true + + - name: ๐ŸŸข Install the latest Node.js + if: steps.skip-check.outputs.skip-check != 'true' + uses: actions/setup-node@v4 + with: + node-version: "latest" + + - name: ๐Ÿšง Install build dependencies + if: steps.skip-check.outputs.skip-check != 'true' + run: | + cd frontend + npm run setup + + - name: ๐Ÿฆ€ Install the latest Rust + if: steps.skip-check.outputs.skip-check != 'true' + run: | + rustup update stable + + - name: ๐Ÿฆ€ Fetch Rust dependencies + if: steps.skip-check.outputs.skip-check != 'true' + run: | + cargo fetch --locked + + - name: ๐ŸŒ Build Graphite web code + if: steps.skip-check.outputs.skip-check != 'true' + env: + NODE_ENV: production + run: mold -run cargo run build web + + - name: ๐Ÿ“ค Publish to Cloudflare Pages + if: steps.skip-check.outputs.skip-check != 'true' + uses: cloudflare/pages-action@1 + continue-on-error: true + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + projectName: graphite-dev + directory: frontend/dist + + - name: ๐Ÿ‘• Lint Graphite web formatting + if: steps.skip-check.outputs.skip-check != 'true' + env: + NODE_ENV: production + run: | + cd frontend + npm run lint + + # Run the Rust tests on the self-hosted native runner + test: + runs-on: [self-hosted, target/native] + env: + RUSTC_WRAPPER: /usr/bin/sccache + CARGO_INCREMENTAL: 0 + SCCACHE_DIR: /var/lib/github-actions/.cache + + steps: + - name: ๐Ÿ“ฅ Clone and checkout repository + uses: actions/checkout@v3 + + - name: ๐Ÿšฆ Check if CI can be skipped + id: skip-check + uses: cariad-tech/merge-queue-ci-skipper@main + + - name: ๐Ÿฆ€ Install the latest Rust + if: steps.skip-check.outputs.skip-check != 'true' + run: | + rustup update stable + + - name: ๐Ÿฆ€ Fetch Rust dependencies + if: steps.skip-check.outputs.skip-check != 'true' + run: | + cargo fetch --locked + + - name: ๐Ÿงช Run Rust tests + if: steps.skip-check.outputs.skip-check != 'true' + env: + RUSTFLAGS: -Dwarnings + run: | + mold -run cargo test --all-features diff --git a/.github/workflows/comment-!build-commands.yml b/.github/workflows/comment-!build-commands.yml index 87b8581b19..0224b7224d 100644 --- a/.github/workflows/comment-!build-commands.yml +++ b/.github/workflows/comment-!build-commands.yml @@ -1,7 +1,6 @@ # USAGE: # After reviewing the code, core team members may comment on a PR with the exact text: -# - `!build-dev` to build with debug symbols and optimizations disabled -# - `!build-profiling` to build with debug symbols and optimizations enabled +# - `!build-debug` to build with debug symbols and optimizations disabled # - `!build` to build without debug symbols and optimizations enabled # The comment may not contain any other text, not even whitespace or newlines. name: "!build PR Command" @@ -21,7 +20,7 @@ jobs: if: > github.event.issue.pull_request && github.event.comment.author_association == 'MEMBER' && - (github.event.comment.body == '!build-dev' || github.event.comment.body == '!build-profiling' || github.event.comment.body == '!build') + (github.event.comment.body == '!build-debug' || github.event.comment.body == '!build') runs-on: self-hosted permissions: contents: read @@ -82,22 +81,43 @@ 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 + id: comment_actions_run_link + uses: actions/github-script@v6 + with: + script: | + github.rest.issues.updateComment({ + comment_id: ${{ github.event.comment.id }}, + owner: context.repo.owner, + repo: context.repo.repo, + body: '!build ([Run ID ' + context.runId + '](https://github.com/GraphiteEditor/Graphite/actions/runs/' + context.runId + '))' + }); + - name: ๐ŸŒ Build Graphite web code env: NODE_ENV: production - run: | - cd frontend - mold -run npm run ${{ steps.build_command.outputs.command }} + if: ${{ success() || failure()}} + run: mold -run cargo run ${{ steps.build_command.outputs.command }} + + - name: โ— Warn on build failure + if: ${{ failure() }} + uses: actions/github-script@v6 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'The build process has failed. Please check the [build logs](https://github.com/GraphiteEditor/Graphite/actions/runs/' + context.runId + ') for details.' + }); - name: ๐Ÿ“ค Publish to Cloudflare Pages id: cloudflare @@ -110,6 +130,18 @@ jobs: projectName: graphite-dev directory: frontend/dist + - name: โ— Warn on publish failure + if: ${{ failure() }} + uses: actions/github-script@v6 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'The deployment to Cloudflare Pages has failed. Please check the [build logs](https://github.com/GraphiteEditor/Graphite/actions/runs/' + context.runId + ') for details. + }); + - name: ๐Ÿ’ฌ Comment build link uses: actions/github-script@v6 with: diff --git a/.github/workflows/comment-profiling-changes.yaml b/.github/workflows/comment-profiling-changes.yaml index 76b12265a1..70877a5e41 100644 --- a/.github/workflows/comment-profiling-changes.yaml +++ b/.github/workflows/comment-profiling-changes.yaml @@ -3,9 +3,9 @@ name: Profiling Changes on: pull_request: paths: - - 'node-graph/**' - - 'Cargo.toml' - - 'Cargo.lock' + - "node-graph/**" + - "Cargo.toml" + - "Cargo.lock" env: CARGO_TERM_COLOR: always @@ -200,7 +200,7 @@ jobs: let commentBody = ""; function formatNumber(num) { - return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ","); } function formatPercentage(pct) { diff --git a/.github/workflows/build-dev-and-ci.yml b/.github/workflows/deploy-master.yml similarity index 60% rename from .github/workflows/build-dev-and-ci.yml rename to .github/workflows/deploy-master.yml index 365eb76fe8..bf6c280b22 100644 --- a/.github/workflows/build-dev-and-ci.yml +++ b/.github/workflows/deploy-master.yml @@ -1,21 +1,20 @@ -name: "Editor: Dev & CI" +name: "Deploy Master" on: push: branches: - master - pull_request: {} + env: CARGO_TERM_COLOR: always INDEX_HTML_HEAD_REPLACEMENT: jobs: - build: - runs-on: self-hosted + deploy: + runs-on: [self-hosted, target/wasm] permissions: contents: write deployments: write - pull-requests: write actions: write env: RUSTC_WRAPPER: /usr/bin/sccache @@ -41,29 +40,16 @@ jobs: - name: ๐Ÿฆ€ Install the latest Rust run: | - echo "Initial system version:" - rustc --version rustup update stable - 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) - git rev-parse --abbrev-ref HEAD | grep master > /dev/null || export INDEX_HTML_HEAD_REPLACEMENT="" sed -i "s||$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html - 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 @@ -77,7 +63,6 @@ jobs: directory: frontend/dist - name: ๐Ÿ’ฌ Comment build link URL to commit hash page on GitHub - if: github.ref == 'refs/heads/master' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -89,36 +74,15 @@ jobs: |-| | ${{ steps.cloudflare.outputs.url }} |" - - name: ๐Ÿ‘• Lint Graphite web formatting - env: - NODE_ENV: production - run: | - cd frontend - npm run lint - - - name: ๐Ÿ”ฌ Check Rust formatting - run: | - mold -run cargo fmt --all -- --check - - - name: ๐Ÿฆ€ Build Rust code - env: - RUSTFLAGS: -Dwarnings - run: | - mold -run cargo build --all-features - - - name: ๐Ÿงช Run Rust tests - run: | - 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' id: cache-website-code-docs uses: actions/cache/restore@v3 with: @@ -126,7 +90,6 @@ jobs: key: website-code-docs - name: ๐Ÿ” Check if auto-generated code docs artifacts changed - if: github.ref == 'refs/heads/master' id: website-code-docs-changed run: | if ! diff --brief --recursive artifacts-generated artifacts; then @@ -154,31 +117,3 @@ jobs: run: | rm -rf artifacts gh workflow run website.yml --ref master - - # miri: - # runs-on: self-hosted - - # steps: - # - uses: actions/checkout@v3 - - # - name: ๐Ÿงช Run Rust miri - # run: | - # mold -run cargo +nightly miri nextest run -j32 --all-features - - cargo-deny: - runs-on: ubuntu-latest - - steps: - - name: ๐Ÿ“ฅ Clone and checkout repository - uses: actions/checkout@v3 - - - name: ๐Ÿ“œ Check crate license compatibility for root workspace - uses: EmbarkStudios/cargo-deny-action@v2 - with: - command: check bans licenses sources - - - name: ๐Ÿ“œ Check crate license compatibility for /libraries/rawkit - uses: EmbarkStudios/cargo-deny-action@v2 - with: - command: check bans licenses sources - manifest-path: libraries/rawkit/Cargo.toml diff --git a/.github/workflows/provide-shaders.yml b/.github/workflows/provide-shaders.yml new file mode 100644 index 0000000000..b6006a7b24 --- /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 .#graphite-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 9cd800857a..3b8fb54eeb 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -37,7 +37,7 @@ jobs: - 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 @@ -67,19 +67,27 @@ 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 diff --git a/.gitignore b/.gitignore index f410829a7b..06bcee0483 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ branding/ target/ +third-party-licenses.txt* result/ +.flatpak-builder/ *.spv *.exrc perf.data* @@ -10,5 +12,3 @@ flamegraph.svg .idea/ .direnv .DS_Store -hierarchical_message_system_tree.txt -hierarchical_message_system_tree.html diff --git a/.nix/default.nix b/.nix/default.nix new file mode 100644 index 0000000000..e77cc4a60d --- /dev/null +++ b/.nix/default.nix @@ -0,0 +1,81 @@ +inputs: + +let + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + forAllSystems = f: inputs.nixpkgs.lib.genAttrs systems (system: f system); + args = + system: + ( + let + lib = inputs.nixpkgs.lib // { + call = p: import p args; + }; + + pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ (import inputs.rust-overlay) ]; + }; + + info = { + pname = "graphite"; + version = "unstable"; + src = inputs.nixpkgs.lib.cleanSourceWith { + src = ./..; + filter = path: type: !(type == "directory" && builtins.baseNameOf path == ".nix"); + }; + cargoVendored = deps.crane.lib.vendorCargoDeps { inherit (info) src; }; + }; + + deps = { + crane = lib.call ./deps/crane.nix; + cef = lib.call ./deps/cef.nix; + rustGPU = lib.call ./deps/rust-gpu.nix; + }; + + args = { + inherit system; + inherit (inputs) self; + inherit inputs; + inherit pkgs; + inherit lib; + inherit info; + inherit deps; + } + // inputs; + in + args + ); + withArgs = f: forAllSystems (system: f (args system)); +in +{ + packages = withArgs ( + { lib, ... }: + rec { + default = graphite; + graphite = (lib.call ./pkgs/graphite.nix) { }; + graphite-dev = (lib.call ./pkgs/graphite.nix) { dev = true; }; + graphite-raster-nodes-shaders = lib.call ./pkgs/graphite-raster-nodes-shaders.nix; + graphite-branding = lib.call ./pkgs/graphite-branding.nix; + graphite-bundle = lib.call ./pkgs/graphite-bundle.nix; + graphite-flatpak-manifest = lib.call ./pkgs/graphite-flatpak-manifest.nix; + + # TODO: graphene-cli = lib.call ./pkgs/graphene-cli.nix; + + tools = { + third-party-licenses = lib.call ./pkgs/tools/third-party-licenses.nix; + }; + } + ); + + devShells = withArgs ( + { lib, ... }: + { + default = lib.call ./dev.nix; + } + ); + + formatter = withArgs ({ pkgs, ... }: pkgs.nixfmt-tree); +} diff --git a/.nix/deps/cef.nix b/.nix/deps/cef.nix index f31219be53..c0ce387cbf 100644 --- a/.nix/deps/cef.nix +++ b/.nix/deps/cef.nix @@ -1,25 +1,26 @@ -{ pkgs, inputs, ... }: +{ pkgs, ... }: let - cef = pkgs.cef-binary.overrideAttrs (_: _: { + cefPath = pkgs.cef-binary.overrideAttrs (finalAttrs: { postInstall = '' - strip $out/Release/*.so* + rm -r $out/* $out/.* || true + strip ./Release/*.so* + mv ./Release/* $out/ + find "./Resources/locales" -maxdepth 1 -type f ! -name 'en-US.pak' -delete + mv ./Resources/* $out/ + mv ./include $out/ + + cat ./CREDITS.html | ${pkgs.xz}/bin/xz -9 -e -c > $out/CREDITS.html.xz + + echo '${ + builtins.toJSON { + type = "minimal"; + name = builtins.baseNameOf finalAttrs.src.url; + sha1 = ""; + } + }' > $out/archive.json ''; }); - - 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/rust-gpu.nix b/.nix/deps/rust-gpu.nix index 2970d515a2..a44f70b368 100644 --- a/.nix/deps/rust-gpu.nix +++ b/.nix/deps/rust-gpu.nix @@ -1,4 +1,4 @@ -{ pkgs, inputs, ... }: +{ pkgs, ... }: let extensions = [ diff --git a/.nix/dev.nix b/.nix/dev.nix index d80d7793e6..e9333d4940 100644 --- a/.nix/dev.nix +++ b/.nix/dev.nix @@ -1,16 +1,58 @@ -{ - pkgs, - deps, - libs, - tools, - ... -}: +{ pkgs, deps, ... }: +let + libs = [ + pkgs.wayland + pkgs.vulkan-loader + pkgs.libGL + pkgs.openssl + pkgs.libraw + + # X11 Support + pkgs.libxkbcommon + pkgs.libXcursor + pkgs.libxcb + pkgs.libX11 + ]; +in pkgs.mkShell ( { - packages = tools.all ++ libs.all; + packages = libs ++ [ + pkgs.pkg-config - LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath libs.all}:${deps.cef.env.CEF_PATH}"; + pkgs.lld + pkgs.nodejs + pkgs.nodePackages.npm + pkgs.binaryen + pkgs.wasm-bindgen-cli_0_2_100 + pkgs.wasm-pack + pkgs.cargo-about + + pkgs.rustc + pkgs.cargo + pkgs.rust-analyzer + pkgs.clippy + pkgs.rustfmt + + pkgs.git + + pkgs.cargo-watch + pkgs.cargo-nextest + pkgs.cargo-expand + + # Linker + pkgs.mold + + # Profiling tools + pkgs.gnuplot + pkgs.samply + pkgs.cargo-flamegraph + + # Plotting tools + pkgs.graphviz + ]; + + LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath libs}:${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 = '' diff --git a/.nix/flake.lock b/.nix/flake.lock deleted file mode 100644 index 2d6d42777a..0000000000 --- a/.nix/flake.lock +++ /dev/null @@ -1,113 +0,0 @@ -{ - "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, - "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" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1764242076, - "narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "crane": "crane", - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1764297505, - "narHash": "sha256-qrLpVu2/hA9Cu6IovMEsgh9YRyvmmWS+bSx7C1JGChA=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "9623580f8ce09ec444b9aca107566ec5db110e62", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/.nix/flake.nix b/.nix/flake.nix deleted file mode 100644 index 83f8c448b2..0000000000 --- a/.nix/flake.nix +++ /dev/null @@ -1,159 +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"; - }; - 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 = - inputs: - inputs.flake-utils.lib.eachDefaultSystem ( - system: - let - info = { - pname = "graphite"; - version = "unstable"; - src = ./..; - }; - - pkgs = import inputs.nixpkgs { - inherit system; - overlays = [ (import inputs.rust-overlay) ]; - }; - - 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; }; - }; - - 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; - }; - - 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 - - pkgs.git - - pkgs.cargo-watch - pkgs.cargo-nextest - pkgs.cargo-expand - - # Linker - pkgs.mold - - # Profiling tools - pkgs.gnuplot - pkgs.samply - pkgs.cargo-flamegraph - - # Plotting tools - pkgs.graphviz - ]; - all = desktop ++ frontend ++ dev; - }; - in - { - 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; - }; - #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 - ; - }; - - default = graphite; - }; - - devShells.default = import ./dev.nix { - inherit - pkgs - deps - libs - tools - ; - }; - - formatter = pkgs.nixfmt-tree; - } - ); -} diff --git a/.nix/pkgs/graphite-branding.nix b/.nix/pkgs/graphite-branding.nix new file mode 100644 index 0000000000..85b53791f8 --- /dev/null +++ b/.nix/pkgs/graphite-branding.nix @@ -0,0 +1,20 @@ +{ info, pkgs, ... }: + +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; + } + ); +in +pkgs.runCommand "${info.pname}-branding" { } '' + mkdir -p $out + tar -xvf ${brandingTar} -C $out --strip-components 1 +'' diff --git a/.nix/pkgs/graphite-bundle.nix b/.nix/pkgs/graphite-bundle.nix new file mode 100644 index 0000000000..d50b00fccd --- /dev/null +++ b/.nix/pkgs/graphite-bundle.nix @@ -0,0 +1,92 @@ +{ + pkgs, + self, + system, + ... +}: +let + bundle = + { + archive ? false, + compression ? null, + passthru ? { }, + }: + ( + let + graphite = self.packages.${system}.graphite; + 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 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' \ + --remove-needed libGL.so \ + out/bin/graphite + cp -r ${graphite}/share out/share + 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* + ''; + 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 { + passthru = { + tar = bundle { + archive = true; + passthru = { + gz = bundle { + compression = "gz"; + }; + xz = bundle { + compression = "xz"; + }; + }; + }; + }; +} diff --git a/.nix/pkgs/graphite-flatpak-manifest.nix b/.nix/pkgs/graphite-flatpak-manifest.nix new file mode 100644 index 0000000000..fee6603119 --- /dev/null +++ b/.nix/pkgs/graphite-flatpak-manifest.nix @@ -0,0 +1,39 @@ +{ + pkgs, + self, + system, + ... +}: + +(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 = self.packages.${system}.graphite-bundle.tar; + strip-components = 0; + } + ]; + } + ]; +} diff --git a/.nix/pkgs/raster-nodes-shaders.nix b/.nix/pkgs/graphite-raster-nodes-shaders.nix similarity index 78% rename from .nix/pkgs/raster-nodes-shaders.nix rename to .nix/pkgs/graphite-raster-nodes-shaders.nix index 46e2f90a29..727e8c9555 100644 --- a/.nix/pkgs/raster-nodes-shaders.nix +++ b/.nix/pkgs/graphite-raster-nodes-shaders.nix @@ -1,12 +1,4 @@ -{ - info, - pkgs, - inputs, - deps, - libs, - tools, - ... -}: +{ info, deps, ... }: (deps.crane.lib.overrideToolchain (_: deps.rustGPU.toolchain)).buildPackage { pname = "raster-nodes-shaders"; @@ -16,7 +8,7 @@ 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" + "${deps.rustGPU.toolchain.availableComponents.rust-src}/lib/rustlib/src/rust/library/Cargo.lock" ]; }; diff --git a/.nix/pkgs/graphite.nix b/.nix/pkgs/graphite.nix index 31fd507254..bd757add80 100644 --- a/.nix/pkgs/graphite.nix +++ b/.nix/pkgs/graphite.nix @@ -1,61 +1,58 @@ { info, pkgs, - inputs, + self, deps, - libs, - tools, + system, + lib, ... }: { - 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 - ''; + branding = self.packages.${system}.graphite-branding; + cargoVendorDir = deps.crane.lib.vendorCargoDeps { inherit (info) src; }; resourcesCommon = { pname = "${info.pname}-resources"; inherit (info) version src; + inherit cargoVendorDir; strictDeps = true; - doCheck = false; - nativeBuildInputs = tools.frontend; + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.lld + pkgs.nodejs + pkgs.nodePackages.npm + pkgs.binaryen + pkgs.wasm-bindgen-cli_0_2_100 + pkgs.wasm-pack + pkgs.cargo-about + ]; + buildInputs = [ pkgs.openssl ]; env.CARGO_PROFILE = if dev then "dev" else "release"; cargoExtraArgs = "--target wasm32-unknown-unknown -p graphite-wasm --no-default-features --features native"; + doCheck = false; }; resources = deps.crane.lib.buildPackage ( resourcesCommon // { cargoArtifacts = deps.crane.lib.buildDepsOnly resourcesCommon; - # TODO: Remove the need for this hash by using individual package resolutions and hashes from package-lock.json - npmDeps = pkgs.fetchNpmDeps { - inherit (info) pname version; - src = "${info.src}/frontend"; - hash = "sha256-D8VCNK+Ca3gxO+5wriBn8FszG8/x8n/zM6/MPo9E2j4="; + npmDeps = pkgs.importNpmLock { + npmRoot = "${info.src}/frontend"; }; npmRoot = "frontend"; npmConfigScript = "setup"; makeCacheWritable = true; - nativeBuildInputs = tools.frontend ++ [ pkgs.npmHooks.npmConfigHook ]; + nativeBuildInputs = [ + pkgs.importNpmLock.npmConfigHook + pkgs.removeReferencesTo + ] + ++ resourcesCommon.nativeBuildInputs; prePatch = '' mkdir branding @@ -75,19 +72,39 @@ let mkdir -p $out cp -r frontend/dist/* $out/ ''; + + postFixup = '' + find "$out" -type f -exec remove-references-to -t "${cargoVendorDir}" '{}' + + ''; } ); + libs = [ + pkgs.wayland + pkgs.vulkan-loader + pkgs.libGL + pkgs.openssl + pkgs.libraw + + # X11 Support + pkgs.libxkbcommon + pkgs.libXcursor + pkgs.libxcb + pkgs.libX11 + ]; common = { inherit (info) pname version src; + inherit cargoVendorDir; strictDeps = true; - buildInputs = libs.desktop-all; - nativeBuildInputs = tools.desktop ++ [ pkgs.makeWrapper ]; + buildInputs = libs; + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.cargo-about + pkgs.removeReferencesTo + ]; 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" - }"; + cargoExtraArgs = "-p graphite-desktop"; doCheck = false; }; in @@ -97,24 +114,34 @@ deps.crane.lib.buildPackage ( // { cargoArtifacts = deps.crane.lib.buildDepsOnly common; - env = - common.env - // { - RASTER_NODES_SHADER_PATH = pkgs.raster-nodes-shaders; - } - // ( - if embeddedResources then - { - EMBEDDED_RESOURCES = resources; - } - else - { } - ); + env = common.env // { + RASTER_NODES_SHADER_PATH = self.packages.${system}.graphite-raster-nodes-shaders; + EMBEDDED_RESOURCES = resources; + GRAPHITE_GIT_COMMIT_HASH = self.rev or "unknown"; + GRAPHITE_GIT_COMMIT_DATE = self.lastModified or "unknown"; + }; - postUnpack = '' - mkdir ./branding - cp -r ${branding}/* ./branding - ''; + npmDeps = pkgs.importNpmLock { + npmRoot = "${info.src}/frontend"; + }; + npmRoot = "frontend"; + nativeBuildInputs = [ + pkgs.importNpmLock.npmConfigHook + pkgs.nodePackages.npm + ] + ++ common.nativeBuildInputs; + + preBuild = '' + ${lib.getExe self.packages.${system}.tools.third-party-licenses} + '' + + ( + if 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 @@ -124,13 +151,22 @@ deps.crane.lib.buildPackage ( 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/ + 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}" + remove-references-to -t "${cargoVendorDir}" $out/bin/graphite + + patchelf \ + --set-rpath "${pkgs.lib.makeLibraryPath libs}:${deps.cef.env.CEF_PATH}" \ + --add-needed libGL.so \ + $out/bin/graphite ''; } ) diff --git a/.nix/pkgs/tools/third-party-licenses.nix b/.nix/pkgs/tools/third-party-licenses.nix new file mode 100644 index 0000000000..9559603377 --- /dev/null +++ b/.nix/pkgs/tools/third-party-licenses.nix @@ -0,0 +1,31 @@ +{ + info, + deps, + pkgs, + ... +}: + +let + cargoVendorDir = deps.crane.lib.vendorCargoDeps { inherit (info) src; }; + common = { + pname = "third-party-licenses"; + inherit (info) version src; + inherit cargoVendorDir; + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ pkgs.openssl ]; + strictDeps = true; + env = deps.cef.env // { + CARGO_PROFILE = "dev"; + }; + cargoExtraArgs = "-p third-party-licenses --features desktop"; + doCheck = false; + }; +in +deps.crane.lib.buildPackage ( + common + // { + inherit cargoVendorDir; + cargoArtifacts = deps.crane.lib.buildDepsOnly common; + meta.mainProgram = "third-party-licenses"; + } +) diff --git a/.nix/shell.nix b/.nix/shell.nix deleted file mode 100644 index 5b13af2e20..0000000000 --- a/.nix/shell.nix +++ /dev/null @@ -1,28 +0,0 @@ -# This is a helper file for people using NixOS as their operating system. -# If you don't know what this file does, you can safely ignore it. - -# If you are using Nix as your package manager, you can run 'nix-shell .nix' -# in the root directory of the project and Nix will open a bash shell -# with all the packages needed to build and run Graphite installed. -# A shell.nix file is used in the Nix ecosystem to define a development -# environment with specific dependencies. When you enter a Nix shell using -# this file, it ensures that all the specified tools and libraries are -# available regardless of the host system's configuration. This provides -# a reproducible development environment across different machines and developers. - -# You can enter the Nix shell and run Graphite like normal with: -# > npm start -# Or you can run it like this without needing to first enter the Nix shell: -# > nix-shell .nix --command "npm start" - -# Uses flake compat to provide a development shell that is identical to the one defined in the flake -(import ( - let - lock = builtins.fromJSON (builtins.readFile ./flake.lock); - nodeName = lock.nodes.root.inputs.flake-compat; - in - fetchTarball { - url = lock.nodes.${nodeName}.locked.url; - sha256 = lock.nodes.${nodeName}.locked.narHash; - } -) { src = ./.; }).shellNix diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 25148f729c..dd8c135ef9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -11,9 +11,10 @@ // Code quality "wayou.vscode-todo-highlight", "streetsidesoftware.code-spell-checker", - // Helpful + // Git "mhutchie.git-graph", "qezhu.gitlink", + // Helpful "wmaurer.change-case" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index a9ae607985..d1c8a4ae1d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,12 +26,17 @@ // 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"], @@ -47,13 +52,36 @@ "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` + "a11y_no_static_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*(?