mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 07:18:04 +08:00
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:
11
tools/cargo-run/Cargo.toml
Normal file
11
tools/cargo-run/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "cargo-run"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
default-run = "cargo-run"
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
104
tools/cargo-run/src/lib.rs
Normal file
104
tools/cargo-run/src/lib.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
|
||||
pub mod requirements;
|
||||
|
||||
pub enum Action {
|
||||
Run,
|
||||
Build,
|
||||
}
|
||||
|
||||
pub enum Target {
|
||||
Web,
|
||||
Desktop,
|
||||
Cli,
|
||||
}
|
||||
|
||||
pub enum Profile {
|
||||
Default,
|
||||
Release,
|
||||
Debug,
|
||||
}
|
||||
|
||||
pub struct Task {
|
||||
pub action: Action,
|
||||
pub target: Target,
|
||||
pub profile: Profile,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn parse(args: &[&str]) -> Option<Self> {
|
||||
let split = args.iter().position(|a| *a == "--").unwrap_or(args.len());
|
||||
let passthru_args = args[split..].iter().skip(1).map(|s| s.to_string()).collect();
|
||||
let args = &args[..split];
|
||||
|
||||
let (action, args) = match args.first() {
|
||||
Some(&"build") => (Action::Build, &args[1..]),
|
||||
Some(&"run") => (Action::Run, &args[1..]),
|
||||
Some(&"help") => return None,
|
||||
_ => (Action::Run, args),
|
||||
};
|
||||
|
||||
let (target, args) = match args.first() {
|
||||
Some(&"desktop") => (Target::Desktop, &args[1..]),
|
||||
Some(&"web") => (Target::Web, &args[1..]),
|
||||
Some(&"cli") => (Target::Cli, &args[1..]),
|
||||
_ => (Target::Web, args),
|
||||
};
|
||||
|
||||
let profile = match args.first() {
|
||||
Some(&"release") => Profile::Release,
|
||||
Some(&"debug") => Profile::Debug,
|
||||
None => Profile::Default,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(Task {
|
||||
target,
|
||||
action,
|
||||
profile,
|
||||
args: passthru_args,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(command: &str) -> Result<(), Error> {
|
||||
run_from(command, None)
|
||||
}
|
||||
|
||||
pub fn npm_run_in_frontend_dir(args: &str) -> Result<(), Error> {
|
||||
let workspace_dir = std::path::PathBuf::from(env!("CARGO_WORKSPACE_DIR"));
|
||||
let frontend_dir = workspace_dir.join("frontend");
|
||||
let npm = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" };
|
||||
run_from(&format!("{npm} run {args}"), Some(&frontend_dir))
|
||||
}
|
||||
|
||||
fn run_from(command: &str, dir: Option<&PathBuf>) -> Result<(), Error> {
|
||||
let command = command.split_whitespace().collect::<Vec<_>>();
|
||||
let mut cmd = process::Command::new(command[0]);
|
||||
if command.len() > 1 {
|
||||
cmd.args(&command[1..]);
|
||||
}
|
||||
if let Some(dir) = dir {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
let exit_code = cmd
|
||||
.spawn()
|
||||
.map_err(|e| Error::Io(e, format!("Failed to spawn command '{}'", command.join(" "))))?
|
||||
.wait()
|
||||
.map_err(|e| Error::Io(e, format!("Failed to wait for command '{}'", command.join(" "))))?;
|
||||
if !exit_code.success() {
|
||||
return Err(Error::Command(command.join(" "), exit_code));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("{1}: {0}")]
|
||||
Io(#[source] std::io::Error, String),
|
||||
|
||||
#[error("Command '{0}' exited with code {1}")]
|
||||
Command(String, process::ExitStatus),
|
||||
}
|
||||
99
tools/cargo-run/src/main.rs
Normal file
99
tools/cargo-run/src/main.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use std::process::ExitCode;
|
||||
|
||||
use cargo_run::*;
|
||||
|
||||
fn usage() {
|
||||
println!();
|
||||
println!("USAGE:");
|
||||
println!(" cargo run [<command>] [<target>] [<profile>] [-- [args]...]");
|
||||
println!();
|
||||
println!("COMMON USAGE:");
|
||||
println!(" cargo run Run the web app");
|
||||
println!(" cargo run desktop Run the desktop app");
|
||||
println!();
|
||||
println!("OPTIONS:");
|
||||
println!("<command>:");
|
||||
println!(" [run] Run the selected target (default)");
|
||||
println!(" build Build the selected target");
|
||||
println!(" help Show this message");
|
||||
println!("<target>:");
|
||||
println!(" [web] Web app (default)");
|
||||
println!(" desktop Desktop app");
|
||||
println!(" cli Graphene CLI");
|
||||
println!("<profile>:");
|
||||
println!(" [debug] Optimizations disabled (default for run)");
|
||||
println!(" [release] Optimizations enabled (default for build)");
|
||||
println!();
|
||||
println!("MORE EXAMPLES:");
|
||||
println!(" cargo run build desktop");
|
||||
println!(" cargo run desktop release");
|
||||
println!(" cargo run cli -- --help");
|
||||
println!()
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let args: Vec<&str> = args.iter().skip(1).map(String::as_str).collect();
|
||||
|
||||
let task = match Task::parse(&args) {
|
||||
Some(run) => run,
|
||||
None => {
|
||||
usage();
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = run_task(&task) {
|
||||
eprintln!("Error: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
fn run_task(task: &Task) -> Result<(), Error> {
|
||||
requirements::check(task)?;
|
||||
|
||||
match (&task.action, &task.target, &task.profile) {
|
||||
(Action::Run, Target::Web, Profile::Debug | Profile::Default) => npm_run_in_frontend_dir("start")?,
|
||||
(Action::Run, Target::Web, Profile::Release) => npm_run_in_frontend_dir("production")?,
|
||||
|
||||
(Action::Build, Target::Web, Profile::Debug) => npm_run_in_frontend_dir("build-dev")?,
|
||||
(Action::Build, Target::Web, Profile::Release | Profile::Default) => npm_run_in_frontend_dir("build")?,
|
||||
|
||||
(action, Target::Desktop, mut profile) => {
|
||||
if matches!(profile, Profile::Default) {
|
||||
profile = match action {
|
||||
Action::Run => &Profile::Debug,
|
||||
Action::Build => &Profile::Release,
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(profile, Profile::Release) {
|
||||
npm_run_in_frontend_dir("build-native")?;
|
||||
} else {
|
||||
npm_run_in_frontend_dir("build-native-dev")?;
|
||||
};
|
||||
|
||||
run("cargo run -p third-party-licenses --features desktop")?;
|
||||
|
||||
let cargo_profile = match profile {
|
||||
Profile::Debug => "dev",
|
||||
Profile::Release => "release",
|
||||
Profile::Default => unreachable!(),
|
||||
};
|
||||
let args = if matches!(action, Action::Run) {
|
||||
format!(" -- open {}", task.args.join(" "))
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
run(&format!("cargo run --profile {cargo_profile} -p graphite-desktop-bundle{args}"))?;
|
||||
}
|
||||
|
||||
(Action::Run, Target::Cli, Profile::Debug | Profile::Default) => run(&format!("cargo run -p graphene-cli -- {}", task.args.join(" ")))?,
|
||||
(Action::Run, Target::Cli, Profile::Release) => run(&format!("cargo run -r -p graphene-cli -- {}", task.args.join(" ")))?,
|
||||
|
||||
(Action::Build, Target::Cli, Profile::Debug) => run("cargo build -p graphene-cli")?,
|
||||
(Action::Build, Target::Cli, Profile::Release | Profile::Default) => run("cargo build -r -p graphene-cli")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
196
tools/cargo-run/src/requirements.rs
Normal file
196
tools/cargo-run/src/requirements.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
use std::io::IsTerminal;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct Requirement {
|
||||
command: &'static str,
|
||||
args: &'static [&'static str],
|
||||
name: &'static str,
|
||||
version: Option<&'static str>,
|
||||
install: Option<&'static str>,
|
||||
skip: Option<&'static dyn Fn(&Task) -> bool>,
|
||||
}
|
||||
|
||||
fn requirements(task: &Task) -> Vec<Requirement> {
|
||||
[
|
||||
Requirement {
|
||||
command: "rustc",
|
||||
args: &["--version"],
|
||||
name: "Rust",
|
||||
..Default::default()
|
||||
},
|
||||
Requirement {
|
||||
command: "cargo-about",
|
||||
args: &["--version"],
|
||||
name: "cargo-about",
|
||||
install: Some("cargo install cargo-about"),
|
||||
skip: Some(&|task| matches!(task.target, Target::Cli)),
|
||||
..Default::default()
|
||||
},
|
||||
Requirement {
|
||||
command: "cargo-watch",
|
||||
args: &["--version"],
|
||||
name: "cargo-watch",
|
||||
install: Some("cargo install cargo-watch"),
|
||||
skip: Some(&|task| {
|
||||
!matches!(
|
||||
task,
|
||||
Task {
|
||||
target: Target::Web,
|
||||
action: Action::Run,
|
||||
..
|
||||
}
|
||||
)
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Requirement {
|
||||
command: "wasm-bindgen",
|
||||
args: &["--version"],
|
||||
name: "wasm-bindgen-cli",
|
||||
version: Some("0.2.100"),
|
||||
install: Some("cargo install -f wasm-bindgen-cli@0.2.100"),
|
||||
skip: Some(&|task| matches!(task.target, Target::Cli)),
|
||||
},
|
||||
Requirement {
|
||||
command: "wasm-pack",
|
||||
args: &["--version"],
|
||||
name: "wasm-pack",
|
||||
install: Some("cargo install wasm-pack"),
|
||||
skip: Some(&|task| matches!(task.target, Target::Cli)),
|
||||
..Default::default()
|
||||
},
|
||||
Requirement {
|
||||
command: "node",
|
||||
args: &["--version"],
|
||||
name: "Node.js",
|
||||
skip: Some(&|task| matches!(task.target, Target::Cli)),
|
||||
..Default::default()
|
||||
},
|
||||
Requirement {
|
||||
command: "cmake",
|
||||
args: &["--version"],
|
||||
name: "CMake",
|
||||
skip: Some(&|task| !matches!(task.target, Target::Desktop) || cfg!(target_os = "linux")),
|
||||
..Default::default()
|
||||
},
|
||||
Requirement {
|
||||
command: "ninja",
|
||||
args: &["--version"],
|
||||
name: "Ninja",
|
||||
skip: Some(&|task| !matches!(task.target, Target::Desktop) || cfg!(target_os = "linux")),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
.iter()
|
||||
.filter(|d| if let Some(skip) = d.skip { !skip(task) } else { true })
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn check(task: &Task) -> Result<(), Error> {
|
||||
eprintln!();
|
||||
eprintln!("Checking Requirements:");
|
||||
|
||||
let mut installable: Vec<Requirement> = Vec::new();
|
||||
let mut failures: Vec<String> = Vec::new();
|
||||
|
||||
for dep in requirements(task) {
|
||||
match Command::new(dep.command).args(dep.args).output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let version = String::from_utf8_lossy(&output.stdout);
|
||||
let version = version.lines().next().unwrap_or_default().trim();
|
||||
|
||||
if let Some(expected) = dep.version {
|
||||
if version.contains(expected) {
|
||||
eprintln!(" ✓ {} ({})", dep.name, version);
|
||||
} else {
|
||||
eprintln!(" ✗ {} (found {}, expected {})", dep.name, version, expected);
|
||||
if dep.install.is_some() {
|
||||
installable.push(dep);
|
||||
} else {
|
||||
failures.push(format!("{}: version mismatch (found {version}, expected {expected})", dep.name));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!(" ✓ {} ({})", dep.name, version);
|
||||
}
|
||||
}
|
||||
Ok(output) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
eprintln!(" ✗ {} - command failed: {}", dep.name, stderr.trim());
|
||||
if dep.install.is_some() {
|
||||
installable.push(dep);
|
||||
} else {
|
||||
failures.push(format!("{}: not installed or not working", dep.name));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!(" ✗ {} - not found", dep.name);
|
||||
if dep.install.is_some() {
|
||||
installable.push(dep);
|
||||
} else {
|
||||
failures.push(format!("{}: not found in PATH", dep.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!();
|
||||
|
||||
if installable.is_empty() && failures.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let total = installable.len() + failures.len();
|
||||
eprintln!("{total} requirement{} not met:", if total > 1 { "s" } else { "" });
|
||||
for dep in &installable {
|
||||
eprintln!(" - {}: {}", dep.name, dep.install.unwrap());
|
||||
}
|
||||
for msg in &failures {
|
||||
eprintln!(" - {msg}");
|
||||
}
|
||||
|
||||
if !failures.is_empty() {
|
||||
eprintln!();
|
||||
eprintln!("See: https://graphite.art/volunteer/guide/project-setup/");
|
||||
}
|
||||
|
||||
// Don't prompt for automatic installation if we're not interactive session
|
||||
if !std::io::stdout().is_terminal() || !std::io::stderr().is_terminal() || !std::io::stdin().is_terminal() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if installable.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
eprintln!();
|
||||
eprintln!("The following can be installed automatically:");
|
||||
for dep in &installable {
|
||||
eprintln!(" {}", dep.install.unwrap());
|
||||
}
|
||||
eprintln!();
|
||||
eprint!("Install them now? [Y/n] ");
|
||||
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).map_err(|e| Error::Io(e, "Failed to read from stdin".into()))?;
|
||||
let input = input.trim();
|
||||
|
||||
if input.is_empty() || input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes") {
|
||||
for dep in &installable {
|
||||
let parts: Vec<&str> = dep.install.unwrap().split_whitespace().collect();
|
||||
eprintln!("Running: {}...", dep.install.unwrap());
|
||||
let status = Command::new(parts[0])
|
||||
.args(&parts[1..])
|
||||
.status()
|
||||
.map_err(|e| Error::Io(e, format!("Failed to run '{}'", dep.install.unwrap())))?;
|
||||
if !status.success() {
|
||||
eprintln!("Failed to install {}", dep.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -13,6 +13,7 @@ desktop = ["dep:cef-dll-sys", "dep:scraper"]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
lzma-rust2 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
cef-dll-sys = { workspace = true, optional = true }
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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())))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(¤t_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(¤t_hash_path, hash).unwrap_or_else(|e| {
|
||||
eprintln!("Failed to write hash file {}: {e}", current_hash_path.display());
|
||||
process::exit(1);
|
||||
});
|
||||
fs::write(¤t_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)
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user