Skip to content

Reimplement notice file generation for third-party licenses through Rust, now with CEF credits#3808

Merged
timon-schelling merged 16 commits intomasterfrom
cef-credits
Feb 26, 2026
Merged

Reimplement notice file generation for third-party licenses through Rust, now with CEF credits#3808
timon-schelling merged 16 commits intomasterfrom
cef-credits

Conversation

@timon-schelling
Copy link
Member

@timon-schelling timon-schelling commented Feb 22, 2026

Closes #3730

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @timon-schelling, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the third-party license generation process by introducing a new, unified Rust-based tool. This change streamlines dependency management within the Nix ecosystem, improves the accuracy and reliability of license collection for both Rust and JavaScript dependencies, and integrates the process directly into the build pipeline. The overall impact is a more robust and maintainable system for tracking and presenting third-party software licenses.

Highlights

  • Reimplemented Third-Party License Generation: The system for generating third-party license files has been rearchitected, moving from a JavaScript-based approach using rollup-plugin-license and cargo about with a Handlebars template to a new Rust-based tool (tools/third-party-licenses). This new tool consolidates license collection from Cargo, npm, and CEF dependencies.
  • Nix Flake Refactoring and Modularity: Significant changes were made to the Nix flake configuration, including the removal of flake-utils and systems inputs, and the adoption of a more modular lib.call pattern for defining packages and development shells. This streamlines the Nix dependency management and package definitions.
  • Integrated License Tool into Build Process: The newly developed Rust license generation tool is now integrated directly into the build pipelines for both web and desktop applications. It generates a compressed license file for desktop builds and an uncompressed one for web builds, ensuring licenses are always up-to-date and correctly included.
  • Frontend Build Process Updates: The frontend package.json scripts and vite.config.ts were updated to remove the old JavaScript-based license generation logic and instead invoke the new Rust third-party-licenses tool. This ensures the frontend correctly includes the generated license file.
  • Dependency Updates: Both Rust (Cargo.lock) and JavaScript (frontend/package-lock.json) dependencies were updated to support the new license generation process, including the addition of lzma-rust2, scraper, and license-checker-rseidelsohn.
Changelog
  • .gitignore
    • Added third-party-licenses.txt* to the ignore list.
  • .nix/deps/cef.nix
    • Updated CEF dependency definition to remove inputs and compress CREDITS.html.
  • .nix/deps/rust-gpu.nix
    • Updated rust-gpu dependency definition to remove inputs.
  • .nix/dev.nix
    • Refactored development shell dependencies and removed inputs parameter.
  • .nix/flake.lock
    • Removed unused flake-utils and systems entries and updated other dependency hashes.
  • .nix/flake.nix
    • Refactored Nix flake outputs for modularity and integrated the new license generation tool.
  • .nix/pkgs/graphite-branding.nix
    • Added a new Nix package for handling branding assets.
  • .nix/pkgs/graphite-bundle.nix
    • Updated package parameters for improved Nix flake integration.
  • .nix/pkgs/graphite-flatpak-manifest.nix
    • Updated package parameters and referenced the bundled graphite archive.
  • .nix/pkgs/graphite-raster-nodes-shaders.nix
    • Renamed from raster-nodes-shaders.nix and simplified its parameters.
  • .nix/pkgs/graphite.nix
    • Refactored resource handling, updated build inputs, and integrated the new third-party license tool.
  • .nix/pkgs/tools/third-party-licenses.nix
    • Added a new Nix package definition for the third-party license generation tool.
  • Cargo.lock
    • Updated various Rust dependency versions and added new crates for license generation and parsing.
  • Cargo.toml
    • Added the new third-party-licenses crate to the workspace and updated lzma-rust2 and download-cef dependencies.
  • about.hbs
    • Removed the Handlebars template for cargo about output.
  • desktop/Cargo.toml
    • Added lzma-rust2 dependency.
  • desktop/src/app.rs
    • Added logic to decompress and load third-party license text.
  • desktop/wrapper/src/handle_desktop_wrapper_message.rs
    • Implemented handling for displaying the third-party licenses dialog.
  • desktop/wrapper/src/intercept_frontend_message.rs
    • Added a new frontend message to trigger the display of third-party licenses.
  • desktop/wrapper/src/messages.rs
    • Added new message types for loading and displaying third-party licenses.
  • frontend/package-lock.json
    • Updated frontend dependencies, including adding license-checker-rseidelsohn and removing peer flags.
  • frontend/package.json
    • Modified build scripts to use the new native build mode and added license-checker-rseidelsohn as a dev dependency.
  • frontend/src/state-providers/dialog.ts
    • Removed a development mode check and now always attempts to fetch license text.
  • frontend/vite.config.ts
    • Refactored Vite plugins, removed the old license generation logic, and integrated the new Rust-based license tool.
  • package.json
    • Updated top-level build scripts to incorporate the new third-party license generation step.
  • tools/third-party-licenses/.gitignore
    • Added a new gitignore file for the third-party licenses tool.
  • tools/third-party-licenses/Cargo.toml
    • Added a new Cargo.toml for the third-party-licenses Rust crate.
  • tools/third-party-licenses/build.rs
    • Added a new build script to set the CEF_PATH environment variable.
  • tools/third-party-licenses/src/cargo.rs
    • Added a new module for extracting Cargo license information.
  • tools/third-party-licenses/src/cef.rs
    • Added a new module for extracting CEF license information.
  • tools/third-party-licenses/src/main.rs
    • Added the main logic for the third-party license generation tool.
  • tools/third-party-licenses/src/npm.rs
    • Added a new module for extracting npm license information.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant and well-executed refactoring of the third-party license generation process. It replaces a complex Vite/Rollup plugin setup with a dedicated Rust tool, which improves modularity, clarity, and maintainability. The Nix flake configuration has also been cleaned up by removing flake-utils. My review includes a few suggestions to further improve the robustness and efficiency of the new license generation tool and its related Nix expressions.

Comment on lines +193 to +198
fn filter(licenses: &mut Vec<LicenseEntry>) {
licenses.iter_mut().for_each(|l| {
l.packages.retain(|p| !(p.authors.len() == 1 && p.authors[0].contains("contact@graphite.art")));
});
licenses.retain(|l| !l.packages.is_empty());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The author string "contact@graphite.art" is used to filter out first-party crates. This is a "magic string" that could be defined as a constant for better readability and maintainability. This makes it easier to find and change if the contact email ever changes.

Suggested change
fn filter(licenses: &mut Vec<LicenseEntry>) {
licenses.iter_mut().for_each(|l| {
l.packages.retain(|p| !(p.authors.len() == 1 && p.authors[0].contains("contact@graphite.art")));
});
licenses.retain(|l| !l.packages.is_empty());
}
fn filter(licenses: &mut Vec<LicenseEntry>) {
const FIRST_PARTY_AUTHOR: &str = "contact@graphite.art";
licenses.iter_mut().for_each(|l| {
l.packages.retain(|p| !(p.authors.len() == 1 && p.authors[0].contains(FIRST_PARTY_AUTHOR)));
});
licenses.retain(|l| !l.packages.is_empty());
}

Comment on lines 200 to 215
fn dedup_by_licence_text(vec: Vec<LicenseEntry>) -> Vec<LicenseEntry> {
let mut map: BTreeMap<String, LicenseEntry> = BTreeMap::new();

for entry in vec {
match map.entry(entry.text.clone()) {
std::collections::btree_map::Entry::Occupied(mut e) => {
e.get_mut().packages.extend(entry.packages);
}
std::collections::btree_map::Entry::Vacant(e) => {
e.insert(entry);
}
}
}

map.into_values().collect()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation for deduplicating licenses clones the entire license text to use it as a key in the BTreeMap. For large license texts, this can be inefficient in terms of memory and performance.

A more efficient approach would be to use a hash of the license text as the key in a HashMap. This avoids cloning large strings and should be faster for comparisons.

Suggested change
fn dedup_by_licence_text(vec: Vec<LicenseEntry>) -> Vec<LicenseEntry> {
let mut map: BTreeMap<String, LicenseEntry> = BTreeMap::new();
for entry in vec {
match map.entry(entry.text.clone()) {
std::collections::btree_map::Entry::Occupied(mut e) => {
e.get_mut().packages.extend(entry.packages);
}
std::collections::btree_map::Entry::Vacant(e) => {
e.insert(entry);
}
}
}
map.into_values().collect()
}
fn dedup_by_licence_text(vec: Vec<LicenseEntry>) -> Vec<LicenseEntry> {
let mut map: std::collections::HashMap<String, LicenseEntry> = std::collections::HashMap::new();
for entry in vec {
let key = sha256::digest(entry.text.as_bytes());
match map.entry(key) {
std::collections::hash_map::Entry::Occupied(mut e) => {
e.get_mut().packages.extend(entry.packages);
}
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(entry);
}
}
}
map.into_values().collect()
}

This includes licenses from npm, cargo and newly added cef (parsed from CREDITS.html)
@timon-schelling
Copy link
Member Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request is a significant and well-executed refactoring of the third-party license generation process. It replaces a complex system with a dedicated Rust tool, improving maintainability and robustness. The accompanying Nix expression refactoring is also a welcome cleanup. I have a few suggestions to improve the new Rust tool's code quality and fix a potential bug in the filtering logic.


fn filter(licenses: &mut Vec<LicenseEntry>) {
licenses.iter_mut().for_each(|l| {
l.packages.retain(|p| !(p.authors.len() == 1 && p.authors[0].contains("contact@graphite.art")));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current filtering logic for internal packages only checks if there is exactly one author and that author's string contains contact@graphite.art. This might miss cases where an internal package has multiple authors listed. Using iter().any() would be more robust and correctly filter out packages if any of their authors match the internal contact address.

Suggested change
l.packages.retain(|p| !(p.authors.len() == 1 && p.authors[0].contains("contact@graphite.art")));
l.packages.retain(|p| !p.authors.iter().any(|author| author.contains("contact@graphite.art")));

Comment on lines +127 to +132
.map(|pkg| match &pkg {
Package { name, authors, url: Some(url) } if !authors.is_empty() => format!("{} - [{}] - {}", name, authors.join(", "), url),
Package { name, authors: _, url: Some(url) } => format!("{} - {}", name, url),
Package { name, authors, url: None } if !authors.is_empty() => format!("{} - [{}]", name, authors.join(", ")),
_ => pkg.name.clone(),
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This match statement for formatting the package information is a bit hard to read and maintain due to multiple similar arms with subtle differences. A more imperative approach of building the string step-by-step would be more readable and easier to modify in the future.

Suggested change
.map(|pkg| match &pkg {
Package { name, authors, url: Some(url) } if !authors.is_empty() => format!("{} - [{}] - {}", name, authors.join(", "), url),
Package { name, authors: _, url: Some(url) } => format!("{} - {}", name, url),
Package { name, authors, url: None } if !authors.is_empty() => format!("{} - [{}]", name, authors.join(", ")),
_ => pkg.name.clone(),
})
.map(|pkg| {
let mut line = pkg.name.clone();
if !pkg.authors.is_empty() {
line.push_str(&format!(" - [{}]"), pkg.authors.join(", "));
}
if let Some(url) = &pkg.url {
line.push_str(&format!(" - {}", url));
}
line
})

"The package{} listed here {} licensed under the terms of the {} printed beneath",
if multi { "s" } else { "" },
if multi { "are" } else { "is" },
if let Some(license) = license.name.as_ref() { license.to_string() } else { "license".to_string() }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This expression can be simplified for better readability and conciseness by using as_deref() and unwrap_or().

Suggested change
if let Some(license) = license.name.as_ref() { license.to_string() } else { "license".to_string() }
if let Some(license) = license.name.as_deref() { license.to_string() } else { "license".to_string() }

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 40 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".nix/pkgs/tools/third-party-licenses.nix">

<violation number="1" location=".nix/pkgs/tools/third-party-licenses.nix:24">
P2: `cargoArtifacts` is attached as an output attribute on the derivation rather than being passed as an input to `buildPackage`. The `//` here merges onto the *result* of `buildPackage`, not its arguments. This is inconsistent with the project's own pattern in `graphite.nix` and with crane's documented best practice. The `cargoArtifacts` should be passed into `buildPackage` so it can reuse the pre-built dependency artifacts, matching the established pattern:
```nix
deps.crane.lib.buildPackage (common // {
  cargoArtifacts = deps.crane.lib.buildDepsOnly common;
})
// {
  inherit cargoVendorDir;
  meta.mainProgram = "third-party-licenses";
}
```</violation>
</file>

<file name="tools/third-party-licenses/src/cef.rs">

<violation number="1" location="tools/third-party-licenses/src/cef.rs:75">
P2: Non-deterministic file selection: if both `credits.html` and `credits.html.xz` exist in the directory, `find()` on `read_dir()` picks whichever the OS enumerates first. Consider preferring one format explicitly (e.g., prefer the uncompressed file if it exists, falling back to `.xz`).</violation>
</file>

<file name="tools/third-party-licenses/src/main.rs">

<violation number="1" location="tools/third-party-licenses/src/main.rs:93">
P2: Multiple `std::process::exit(1)` calls bypass Rust's normal stack unwinding. Consider having `main` return `Result` and using `?` to propagate errors, which enables proper cleanup and is more idiomatic.

(Based on your team's feedback about avoiding `exit()` and preferring returning for cleanup.) [FEEDBACK_USED]</violation>

<violation number="2" location="tools/third-party-licenses/src/main.rs:144">
P1: Bug: `header.len()` returns byte count but is mixed with `.chars().count()` (character count) from package lines when computing `max_len` and padding. If the license name contains non-ASCII characters (e.g., accented letters), this will produce misaligned box-drawing output or panic from arithmetic underflow. Use `.chars().count()` consistently.</violation>
</file>

<file name=".nix/flake.nix">

<violation number="1" location=".nix/flake.nix:56">
P2: `info.cargoVendored` is defined but never used anywhere in the codebase. Either this should be consumed by downstream derivations (e.g., via `inherit (info) cargoVendored;` in `graphite.nix`) or removed to avoid dead code and unnecessary evaluation.</violation>
</file>

<file name="frontend/vite.config.ts">

<violation number="1" location="frontend/vite.config.ts:64">
P2: The `buildStart` hook fires during `vite dev` as well, so `cargo run -p third-party-licenses` will execute on every dev server startup, unnecessarily slowing down development. Add `apply: 'build'` to restrict this plugin to production builds only.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

if let Some(license) = license.name.as_ref() { license.to_string() } else { "license".to_string() }
);

let max_len = std::iter::once(header.len()).chain(package_lines.iter().map(|l| l.chars().count())).max().unwrap_or(0);
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Bug: header.len() returns byte count but is mixed with .chars().count() (character count) from package lines when computing max_len and padding. If the license name contains non-ASCII characters (e.g., accented letters), this will produce misaligned box-drawing output or panic from arithmetic underflow. Use .chars().count() consistently.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/third-party-licenses/src/main.rs, line 144:

<comment>Bug: `header.len()` returns byte count but is mixed with `.chars().count()` (character count) from package lines when computing `max_len` and padding. If the license name contains non-ASCII characters (e.g., accented letters), this will produce misaligned box-drawing output or panic from arithmetic underflow. Use `.chars().count()` consistently.</comment>

<file context>
@@ -0,0 +1,229 @@
+			if let Some(license) = license.name.as_ref() { license.to_string() } else { "license".to_string() }
+		);
+
+		let max_len = std::iter::once(header.len()).chain(package_lines.iter().map(|l| l.chars().count())).max().unwrap_or(0);
+
+		let padded_packages: Vec<String> = package_lines
</file context>
Fix with Cubic

process::exit(1);
})
.filter_map(|entry| entry.ok())
.find(|entry| {
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Non-deterministic file selection: if both credits.html and credits.html.xz exist in the directory, find() on read_dir() picks whichever the OS enumerates first. Consider preferring one format explicitly (e.g., prefer the uncompressed file if it exists, falling back to .xz).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/third-party-licenses/src/cef.rs, line 75:

<comment>Non-deterministic file selection: if both `credits.html` and `credits.html.xz` exist in the directory, `find()` on `read_dir()` picks whichever the OS enumerates first. Consider preferring one format explicitly (e.g., prefer the uncompressed file if it exists, falling back to `.xz`).</comment>

<file context>
@@ -0,0 +1,105 @@
+			process::exit(1);
+		})
+		.filter_map(|entry| entry.ok())
+		.find(|entry| {
+			let name = entry.file_name();
+			name.eq_ignore_ascii_case("credits.html") || name.eq_ignore_ascii_case("credits.html.xz")
</file context>
Fix with Cubic

@@ -0,0 +1,229 @@
use std::collections::HashMap;
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Multiple std::process::exit(1) calls bypass Rust's normal stack unwinding. Consider having main return Result and using ? to propagate errors, which enables proper cleanup and is more idiomatic.

(Based on your team's feedback about avoiding exit() and preferring returning for cleanup.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/third-party-licenses/src/main.rs, line 93:

<comment>Multiple `std::process::exit(1)` calls bypass Rust's normal stack unwinding. Consider having `main` return `Result` and using `?` to propagate errors, which enables proper cleanup and is more idiomatic.

(Based on your team's feedback about avoiding `exit()` and preferring returning for cleanup.) </comment>

<file context>
@@ -0,0 +1,229 @@
+	if let Some(parent) = output_path.parent() {
+		fs::create_dir_all(parent).unwrap_or_else(|e| {
+			eprintln!("Failed to create directory {}: {e}", parent.display());
+			std::process::exit(1);
+		});
+	}
</file context>
Fix with Cubic

src = ./..;
filter = path: type: !(type == "directory" && builtins.baseNameOf path == ".nix");
};
cargoVendored = deps.crane.lib.vendorCargoDeps { inherit (info) src; };
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: info.cargoVendored is defined but never used anywhere in the codebase. Either this should be consumed by downstream derivations (e.g., via inherit (info) cargoVendored; in graphite.nix) or removed to avoid dead code and unnecessary evaluation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .nix/flake.nix, line 56:

<comment>`info.cargoVendored` is defined but never used anywhere in the codebase. Either this should be consumed by downstream derivations (e.g., via `inherit (info) cargoVendored;` in `graphite.nix`) or removed to avoid dead code and unnecessary evaluation.</comment>

<file context>
@@ -27,143 +26,85 @@
+                  src = ./..;
+                  filter = path: type: !(type == "directory" && builtins.baseNameOf path == ".nix");
+                };
+                cargoVendored = deps.crane.lib.vendorCargoDeps { inherit (info) src; };
+              };
 
</file context>
Fix with Cubic

encoding: "utf8",
shell: true,
if (mode !== "native") {
plugins.push({
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The buildStart hook fires during vite dev as well, so cargo run -p third-party-licenses will execute on every dev server startup, unnecessarily slowing down development. Add apply: 'build' to restrict this plugin to production builds only.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/vite.config.ts, line 64:

<comment>The `buildStart` hook fires during `vite dev` as well, so `cargo run -p third-party-licenses` will execute on every dev server startup, unnecessarily slowing down development. Add `apply: 'build'` to restrict this plugin to production builds only.</comment>

<file context>
@@ -58,374 +25,63 @@ export default defineConfig(({ mode }) => {
-					encoding: "utf8",
-					shell: true,
+	if (mode !== "native") {
+		plugins.push({
+			name: "third-party-licenses",
+			buildStart() {
</file context>
Suggested change
plugins.push({
plugins.push({
apply: "build",
Fix with Cubic

@timon-schelling timon-schelling added this pull request to the merge queue Feb 26, 2026
@Keavon Keavon removed this pull request from the merge queue due to a manual request Feb 26, 2026
@Keavon Keavon changed the title Reimplement third-party-licenses file generation Reimplement third-party licenses notices file generation through Rust, now with CEF credits Feb 26, 2026
@Keavon Keavon changed the title Reimplement third-party licenses notices file generation through Rust, now with CEF credits Reimplement notice file generation for third-party licenses through Rust, now with CEF credits Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop: parse and include CEF credits

2 participants