diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..a9fd1d1c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + + - name: Install Linux system dependencies (egui/winit/rfd) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev \ + libgl1-mesa-dev libgtk-3-dev + + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - uses: Swatinem/rust-cache@v2 + + - name: Format + if: runner.os == 'Linux' + run: cargo fmt --all -- --check + + - name: Clippy + run: cargo clippy --workspace --all-targets -- -D warnings + + - name: Test + run: cargo test --workspace + + - name: Layering + run: cargo xtask layers + + - name: Assets (licences and attribution) + if: runner.os == 'Linux' + run: cargo xtask assets + + - name: CLI without the MCP server still builds + if: runner.os == 'Linux' + run: cargo check -p printcraft-cli --no-default-features + + wasm: + name: wasm32 check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + with: + key: wasm32 + - name: Check library crates for wasm32 + run: cargo xtask wasm + + deny: + name: dependency licences and advisories + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 + with: + command: check + + # Nightly fuzzing arrives with the M1 fuzz targets (execution plan M0.5 / M1). diff --git a/Cargo.lock b/Cargo.lock index c46fc570..c942a94d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5039,9 +5039,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 10f2ecfb..147a1190 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,15 +13,15 @@ repository = "https://github.com/storytold/printcraft" [workspace.dependencies] # internal -printcraft-geom = { path = "crates/geom" } -printcraft-filters = { path = "crates/filters" } -printcraft-cos = { path = "crates/cos" } -printcraft-crypt = { path = "crates/crypt" } -printcraft-organize = { path = "crates/organize" } -printcraft-render = { path = "crates/render" } -printcraft-engine = { path = "crates/engine" } -printcraft-ui-egui = { path = "crates/ui-egui" } -printcraft-automation = { path = "crates/automation" } +printcraft-geom = { path = "crates/geom", version = "0.1.0" } +printcraft-filters = { path = "crates/filters", version = "0.1.0" } +printcraft-cos = { path = "crates/cos", version = "0.1.0" } +printcraft-crypt = { path = "crates/crypt", version = "0.1.0" } +printcraft-organize = { path = "crates/organize", version = "0.1.0" } +printcraft-render = { path = "crates/render", version = "0.1.0" } +printcraft-engine = { path = "crates/engine", version = "0.1.0" } +printcraft-ui-egui = { path = "crates/ui-egui", version = "0.1.0" } +printcraft-automation = { path = "crates/automation", version = "0.1.0", default-features = false } # pdf hayro = "0.7.1" diff --git a/apps/printcraft-cli/Cargo.toml b/apps/printcraft-cli/Cargo.toml index 67cbb8ee..96111880 100644 --- a/apps/printcraft-cli/Cargo.toml +++ b/apps/printcraft-cli/Cargo.toml @@ -21,4 +21,4 @@ printcraft-automation = { workspace = true } [features] default = ["mcp"] # The opt-in MCP server (`printcraft-cli mcp`). Build with --no-default-features to leave it out. -mcp = [] +mcp = ["printcraft-automation/mcp"] diff --git a/crates/automation/Cargo.toml b/crates/automation/Cargo.toml index f00a8d18..a25612a3 100644 --- a/crates/automation/Cargo.toml +++ b/crates/automation/Cargo.toml @@ -14,5 +14,10 @@ printcraft-render = { workspace = true } printcraft-organize = { workspace = true } serde_json = { workspace = true } png = "0.18" -base64 = "0.22" +base64 = { version = "0.22", optional = true } + +[features] +default = ["mcp"] +# The MCP server module. Opt-in at run time either way; disable to leave it out of the build. +mcp = ["dep:base64"] diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index cff9614a..3f817ac6 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -9,6 +9,7 @@ //! with the origin at the top-left of the displayed page. //! - An optional root directory confines every path a tool reads or writes. +#[cfg(feature = "mcp")] pub mod mcp; mod tools; diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 8a53bd76..b340c73a 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -2,6 +2,7 @@ use std::path::{Path, PathBuf}; +#[cfg(feature = "mcp")] use printcraft_automation::mcp::McpServer; use printcraft_automation::{Automation, Content, ToolError, tools}; use serde_json::{Value, json}; @@ -231,11 +232,13 @@ fn command_list_reports_enablement_and_tools() { // ---- MCP --------------------------------------------------------------------------------------- +#[cfg(feature = "mcp")] fn rpc(server: &mut McpServer, id: u64, method: &str, params: Value) -> Value { let line = json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params }).to_string(); serde_json::from_str(&server.handle_line(&line).expect("a reply")).unwrap() } +#[cfg(feature = "mcp")] #[test] fn mcp_session_over_stdio() { let dir = workdir("mcp"); @@ -268,6 +271,7 @@ fn mcp_session_over_stdio() { assert_eq!(&png[1..4], b"PNG"); } +#[cfg(feature = "mcp")] #[test] fn mcp_errors() { let mut s = McpServer::new(Automation::new()); diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..5e010147 --- /dev/null +++ b/deny.toml @@ -0,0 +1,43 @@ +# cargo-deny policy (M0.4). Run with `cargo xtask deny`; part of `cargo xtask ci`. +# Code dependencies follow the same rule as assets (AGENTS.md): permissive licences only. +# Copyleft (GPL, AGPL, LGPL) is never linked; such tools run only as external oracle processes. + +[graph] +all-features = true + +[licenses] +confidence-threshold = 0.9 +allow = [ + "MIT", + "MIT-0", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib", + "Unicode-3.0", + "BSL-1.0", + "CC0-1.0", +] + +[licenses.private] +ignore = true + +[bans] +multiple-versions = "allow" +wildcards = "deny" +allow-wildcard-paths = true + +[sources] +unknown-registry = "deny" +unknown-git = "deny" + +[advisories] +yanked = "warn" + +# egui's default UI fonts (Ubuntu-Light, Hack, emoji-icon-font): font licences on the AGENTS.md +# §1.2 allowlist, attributed in ATTRIBUTION.toml as dependency-bundled assets. +[[licenses.exceptions]] +crate = "epaint_default_fonts" +allow = ["OFL-1.1", "Ubuntu-font-1.0"] diff --git a/xtask/src/gates.rs b/xtask/src/gates.rs index a0aba86c..133718e2 100644 --- a/xtask/src/gates.rs +++ b/xtask/src/gates.rs @@ -92,6 +92,7 @@ pub fn ci(_: &[String]) -> anyhow::Result<()> { ("layers", Box::new(|| layers(&[]))), ("wasm", Box::new(|| wasm(&[]))), ("assets", Box::new(|| crate::assets::run(&[]))), + ("deny", Box::new(|| deny(&[]))), ]; for (i, (name, f)) in steps.iter().enumerate() { eprintln!("\n=== ci: {name} ==="); @@ -104,6 +105,20 @@ pub fn ci(_: &[String]) -> anyhow::Result<()> { Ok(()) } +/// Dependency licences, bans, sources and advisories (`deny.toml`). Skipped with a hint when +/// cargo-deny is not installed, except under CI (`CI` set), where it is required. +pub fn deny(_: &[String]) -> anyhow::Result<()> { + let installed = Command::new("cargo-deny").arg("--version").output().is_ok_and(|o| o.status.success()); + if !installed { + if std::env::var_os("CI").is_some() { + bail!("cargo-deny is required in CI: cargo install cargo-deny --locked"); + } + eprintln!("deny: SKIPPED, cargo-deny is not installed (cargo install cargo-deny --locked)"); + return Ok(()); + } + run_args(&["deny", "--log-level", "error", "check"]) +} + fn run_args(args: &[&str]) -> anyhow::Result<()> { let mut c = cargo(); c.args(args); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 46b54210..e42506ea 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -14,7 +14,8 @@ type Command = fn(&[String]) -> anyhow::Result<()>; const COMMANDS: &[(&str, &str, Command)] = &[ ("layers", "Enforce the crate dependency layering (plan/architecture.md §3)", gates::layers), ("wasm", "cargo check --target wasm32-unknown-unknown for every crate below L8", gates::wasm), - ("ci", "fmt --check, clippy -D warnings, test, layers, wasm, assets (stops at first failure)", gates::ci), + ("deny", "Dependency licences, bans, sources and advisories (deny.toml; needs cargo-deny)", gates::deny), + ("ci", "fmt --check, clippy -D warnings, test, layers, wasm, assets, deny (stops at first failure)", gates::ci), ("assets", "Enforce the asset policy (AGENTS.md §1) against ATTRIBUTION.toml; --write regenerates ATTRIBUTION.md", assets::run), ("corpus", "Fetch test corpora into corpus/ (git-ignored): pdf.js test PDFs", gates::corpus), ("check", "Robustness sweep over corpus/ with printcraft-cli; fails on crashes or regressions vs xtask/baselines", gates::check),