Replace npm build script with new cargo run tool (#3832)

* move nix flake to root

* cargo run tool

* use thiserror in third-party-licenses tool

* prefere panic over exit

* Add automatic dependency check to cargo run tool

* Skip dependecies that are not needed for the current task

* Fixup

* Fixup

* fix windows

* Fixup

* improve usage text

* Fix linux bundle

* add graphen-cli

* fix build profile

* fix

* release profile should not include debug infos

* Review

* remove profiling profile

was redundent with release

* rename to cargo-run tool

* improve consistency

* rename deps to requirements

* fix

* return success when showing usage
This commit is contained in:
Timon
2026-03-07 13:26:19 +00:00
committed by GitHub
parent 50ef6e15cb
commit 5d22292072
39 changed files with 664 additions and 404 deletions
+10 -15
View File
@@ -1,9 +1,9 @@
use crate::{LicenceSource, LicenseEntry, Package};
use crate::{Error, LicenceSource, LicenseEntry, Package};
use serde::Deserialize;
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::process::{self, Command};
use std::process::Command;
pub struct CargoLicenseSource {}
@@ -14,8 +14,8 @@ impl CargoLicenseSource {
}
impl LicenceSource for CargoLicenseSource {
fn licenses(&self) -> Vec<LicenseEntry> {
parse(run())
fn licenses(&self) -> Result<Vec<LicenseEntry>, Error> {
Ok(parse(run()?))
}
}
@@ -84,23 +84,18 @@ fn parse(parsed: Output) -> Vec<LicenseEntry> {
.collect()
}
fn run() -> Output {
fn run() -> Result<Output, Error> {
let output = Command::new("cargo")
.args(["about", "generate", "--format", "json", "--frozen"])
.current_dir(env!("CARGO_WORKSPACE_DIR"))
.output()
.unwrap_or_else(|e| {
eprintln!("Failed to run cargo about generate: {e}");
process::exit(1)
});
.map_err(|e| Error::Io(e, "Failed to run cargo about generate".into()))?;
if !output.status.success() {
eprintln!("cargo about generate failed:\n{}", String::from_utf8_lossy(&output.stderr));
process::exit(1)
return Err(Error::Command(format!("cargo about generate failed:\n{}", String::from_utf8_lossy(&output.stderr))));
}
serde_json::from_str(&String::from_utf8(output.stdout).expect("cargo about generate should return valid UTF-8")).unwrap_or_else(|e| {
eprintln!("Failed to parse cargo about generate JSON: {e}");
process::exit(1)
})
let stdout = String::from_utf8(output.stdout).map_err(|e| Error::Utf8(e, "cargo about generate returned invalid UTF-8".into()))?;
serde_json::from_str(&stdout).map_err(|e| Error::Json(e, "Failed to parse cargo about generate JSON".into()))
}
+15 -28
View File
@@ -1,11 +1,11 @@
use lzma_rust2::XzReader;
use scraper::{Html, Selector};
use std::fs;
use std::hash::Hash;
use std::io::Read;
use std::path::PathBuf;
use std::{fs, process};
use crate::{LicenceSource, LicenseEntry, Package};
use crate::{Error, LicenceSource, LicenseEntry, Package};
pub struct CefLicenseSource;
@@ -16,15 +16,15 @@ impl CefLicenseSource {
}
impl LicenceSource for CefLicenseSource {
fn licenses(&self) -> Vec<LicenseEntry> {
let html = read();
parse(&html)
fn licenses(&self) -> Result<Vec<LicenseEntry>, Error> {
let html = read()?;
Ok(parse(&html))
}
}
impl Hash for CefLicenseSource {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
read().hash(state)
read().unwrap().hash(state)
}
}
@@ -64,42 +64,29 @@ fn parse(html: &str) -> Vec<LicenseEntry> {
.collect()
}
fn read() -> String {
fn read() -> Result<String, Error> {
let cef_path = PathBuf::from(env!("CEF_PATH"));
let cef_credits = std::fs::read_dir(&cef_path)
.unwrap_or_else(|e| {
eprintln!("Failed to read CEF_PATH directory {}: {e}", cef_path.display());
process::exit(1);
})
.map_err(|e| Error::Io(e, format!("Failed to read CEF_PATH directory {}", cef_path.display())))?
.filter_map(|entry| entry.ok())
.find(|entry| {
let name = entry.file_name();
name.eq_ignore_ascii_case("credits.html") || name.eq_ignore_ascii_case("credits.html.xz")
})
.map(|entry| entry.path())
.unwrap_or_else(|| {
eprintln!("Could not find CREDITS.html or CREDITS.html.xz in {}", cef_path.display());
process::exit(1);
});
.ok_or_else(|| Error::CefCreditsNotFound(cef_path.clone()))?;
let decompress_xz = cef_credits.extension().map(|ext| ext.eq_ignore_ascii_case("xz")).unwrap_or(false);
if decompress_xz {
let file = fs::File::open(&cef_credits).unwrap_or_else(|e| {
eprintln!("Failed to open CEF credits file {}: {e}", cef_credits.display());
process::exit(1);
});
let file = fs::File::open(&cef_credits).map_err(|e| Error::Io(e, format!("Failed to open CEF credits file {}", cef_credits.display())))?;
let mut reader = XzReader::new(file, false);
let mut html = String::new();
reader.read_to_string(&mut html).unwrap_or_else(|e| {
eprintln!("Failed to decompress CEF credits file {}: {e}", cef_credits.display());
process::exit(1);
});
html
reader
.read_to_string(&mut html)
.map_err(|e| Error::Io(e, format!("Failed to decompress CEF credits file {}", cef_credits.display())))?;
Ok(html)
} else {
fs::read_to_string(&cef_credits).unwrap_or_else(|e| {
eprintln!("Failed to read CEF credits file {}: {e}", cef_credits.display());
process::exit(1);
})
fs::read_to_string(&cef_credits).map_err(|e| Error::Io(e, format!("Failed to read CEF credits file {}", cef_credits.display())))
}
}
+46 -34
View File
@@ -1,7 +1,8 @@
use std::collections::HashMap;
use std::fs;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::path::PathBuf;
use std::{fs, process};
use std::process::ExitCode;
mod cargo;
#[cfg(feature = "desktop")]
@@ -13,8 +14,27 @@ use crate::cargo::CargoLicenseSource;
use crate::cef::CefLicenseSource;
use crate::npm::NpmLicenseSource;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{1}: {0}")]
Io(#[source] std::io::Error, String),
#[error("{1}: {0}")]
Json(#[source] serde_json::Error, String),
#[error("{1}: {0}")]
Utf8(#[source] std::string::FromUtf8Error, String),
#[error("{0}")]
Command(String),
#[cfg(feature = "desktop")]
#[error("Could not find CREDITS.html or CREDITS.html.xz in {0}")]
CefCreditsNotFound(PathBuf),
}
pub trait LicenceSource: std::hash::Hash {
fn licenses(&self) -> Vec<LicenseEntry>;
fn licenses(&self) -> Result<Vec<LicenseEntry>, Error>;
}
pub struct LicenseEntry {
@@ -38,7 +58,15 @@ struct Run<'a> {
cef: &'a CefLicenseSource,
}
fn main() {
fn main() -> ExitCode {
if let Err(e) = run() {
eprintln!("Error: {e}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
fn run() -> Result<(), Error> {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace_dir = PathBuf::from(env!("CARGO_WORKSPACE_DIR"));
@@ -71,32 +99,26 @@ fn main() {
if current_hash == fs::read_to_string(&current_hash_path).unwrap_or_default() {
eprintln!("No changes in licenses detected, skipping generation.");
return;
return Ok(());
}
eprintln!("Changes in licenses detected, generating new license file.");
let licenses = merge_filter_dedup_and_sort(vec![
cargo_source.licenses(),
npm_source.licenses(),
cargo_source.licenses()?,
npm_source.licenses()?,
#[cfg(feature = "desktop")]
cef_source.licenses(),
cef_source.licenses()?,
]);
let formatted = format_credits(&licenses);
#[cfg(feature = "desktop")]
let output = compress(&formatted);
let output = compress(&formatted)?;
#[cfg(not(feature = "desktop"))]
let output = formatted.as_bytes().to_vec();
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent).unwrap_or_else(|e| {
eprintln!("Failed to create directory {}: {e}", parent.display());
std::process::exit(1);
});
fs::create_dir_all(parent).map_err(|e| Error::Io(e, format!("Failed to create directory {}", parent.display())))?;
}
fs::write(&output_path, &output).unwrap_or_else(|e| {
eprintln!("Failed to write {}: {e}", &output_path.display());
std::process::exit(1);
});
fs::write(&output_path, &output).map_err(|e| Error::Io(e, format!("Failed to write {}", output_path.display())))?;
run.output = &output;
let hash = {
@@ -105,10 +127,9 @@ fn main() {
format!("{:016x}", hasher.finish())
};
fs::write(&current_hash_path, hash).unwrap_or_else(|e| {
eprintln!("Failed to write hash file {}: {e}", current_hash_path.display());
process::exit(1);
});
fs::write(&current_hash_path, hash).map_err(|e| Error::Io(e, format!("Failed to write hash file {}", current_hash_path.display())))?;
Ok(())
}
fn format_credits(licenses: &Vec<LicenseEntry>) -> String {
@@ -210,20 +231,11 @@ fn dedup_by_licence_text(vec: Vec<LicenseEntry>) -> Vec<LicenseEntry> {
}
#[cfg(feature = "desktop")]
fn compress(content: &str) -> Vec<u8> {
fn compress(content: &str) -> Result<Vec<u8>, Error> {
use std::io::Write;
let mut buf = Vec::new();
let mut writer = lzma_rust2::XzWriter::new(&mut buf, lzma_rust2::XzOptions::default()).unwrap_or_else(|e| {
eprintln!("Failed to create XZ writer: {e}");
std::process::exit(1);
});
writer.write_all(content.as_bytes()).unwrap_or_else(|e| {
eprintln!("Failed to write compressed credits: {e}");
std::process::exit(1);
});
writer.finish().unwrap_or_else(|e| {
eprintln!("Failed to finish XZ compression: {e}");
std::process::exit(1);
});
buf
let mut writer = lzma_rust2::XzWriter::new(&mut buf, lzma_rust2::XzOptions::default()).map_err(|e| Error::Io(e, "Failed to create XZ writer".into()))?;
writer.write_all(content.as_bytes()).map_err(|e| Error::Io(e, "Failed to write compressed credits".into()))?;
writer.finish().map_err(|e| Error::Io(e, "Failed to finish XZ compression".into()))?;
Ok(buf)
}
+8 -16
View File
@@ -1,10 +1,9 @@
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::process;
use std::process::Command;
use crate::{LicenceSource, LicenseEntry, Package};
use crate::{Error, LicenceSource, LicenseEntry, Package};
pub struct NpmLicenseSource {
dir: PathBuf,
@@ -16,8 +15,8 @@ impl NpmLicenseSource {
}
impl LicenceSource for NpmLicenseSource {
fn licenses(&self) -> Vec<LicenseEntry> {
parse(run(&self.dir))
fn licenses(&self) -> Result<Vec<LicenseEntry>, Error> {
Ok(parse(run(&self.dir)?))
}
}
@@ -66,7 +65,7 @@ fn parse(parsed: Output) -> Vec<LicenseEntry> {
.collect()
}
fn run(dir: &std::path::Path) -> Output {
fn run(dir: &std::path::Path) -> Result<Output, Error> {
#[cfg(not(target_os = "windows"))]
let mut cmd = Command::new("npx");
#[cfg(target_os = "windows")]
@@ -74,20 +73,13 @@ fn run(dir: &std::path::Path) -> Output {
cmd.args(["license-checker-rseidelsohn", "--production", "--json"]);
cmd.current_dir(dir);
let output = cmd.output().unwrap_or_else(|e| {
eprintln!("Failed to run npx license-checker-rseidelsohn: {e}");
process::exit(1);
});
let output = cmd.output().map_err(|e| Error::Io(e, "Failed to run npx license-checker-rseidelsohn".into()))?;
if !output.status.success() {
eprintln!("npx license-checker-rseidelsohn failed:\n{}", String::from_utf8_lossy(&output.stderr));
process::exit(1);
return Err(Error::Command(format!("npx license-checker-rseidelsohn failed:\n{}", String::from_utf8_lossy(&output.stderr))));
}
let json_str = String::from_utf8(output.stdout).expect("Invalid UTF-8 from license-checker");
let json_str = String::from_utf8(output.stdout).map_err(|e| Error::Utf8(e, "Invalid UTF-8 from license-checker".into()))?;
serde_json::from_str(&json_str).unwrap_or_else(|e| {
eprintln!("Failed to parse license-checker JSON: {e}");
process::exit(1)
})
serde_json::from_str(&json_str).map_err(|e| Error::Json(e, "Failed to parse license-checker JSON".into()))
}