Files
solidcraft-upstream/xtask/src/book.rs
T
bherbruckandClaude Opus 5.5 6090e89b3f Release under the SolidCraft name: binaries, assets and installers (#383)
* Release under the SolidCraft name: binaries, assets and installers

The desktop binary is now `solidcraft` and the CLI `solidcraft-cli` (the two app packages
are renamed; their folders move with the rest of the rename later). Every release asset is
named solidcraft-<ver>-… / SolidCraft-<ver>-windows-x64.exe, and the installers show the
SolidCraft product name.

Kept so existing installs keep updating (docs/compatibility.md):
- `solvecraft-cli` alias binary in every package, forwarding to solidcraft-cli with a
  deprecation note on stderr (tests/legacy_alias.rs).
- Linux app ID ai.storyteller.solvecraft (Flatpak ID, desktop entry, Wayland app_id).
- MSI UpgradeCode, so the MSI upgrades in place.
- deb Replaces/Conflicts/Provides and rpm Obsoletes/Provides solvecraft.
- An old-named .zsync copy so earlier AppImages find updates; a legacy .flatpakrepo copy.
- .solvecraft file associations; SOLVECRAFT_* fallbacks in the packaging scripts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

* MSI: register the legacy .solvecraft extension without a duplicate open verb

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 16:05:35 -04:00

124 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `cargo xtask book`: generate the book's command reference and screenshots, then build it.
//!
//! The command reference (`docs/book/src/reference/commands.md`) comes from
//! `solvecraft-cli commands`, so it can't drift from the registry. The screenshots in
//! `docs/book/src/generated/` are CPU renders by `solvecraft-cli snapshot` of `examples/*.json`
//! and `docs/book/scripts/*.json`, plus the app screenshot from `docs/screenshots/`. Both are
//! gitignored and attributed in ATTRIBUTION.md. The book itself is built by `mdbook` (a separate
//! tool, not a dependency) into `target/book`.
use std::path::Path;
use serde_json::Value;
use crate::cargo;
/// Toolbar tabs in toolbar order; commands not on the toolbar come last, grouped by id prefix.
const TABS: &[&str] = &["SOLID", "SKETCH", "SURFACE", "SHEET METAL", "PLASTIC"];
const SNAPSHOT_SIZE: (&str, &str) = ("960", "600");
pub fn run(root: &Path, build: bool) -> Result<(), String> {
let book = root.join("docs/book");
let mut c = cargo();
c.args(["build", "-q", "--release", "-p", "solidcraft-cli"]);
crate::run(c, "cargo build -p solidcraft-cli")?;
let out = cli().arg("commands").output().map_err(|e| format!("solvecraft-cli: {e}"))?;
if !out.status.success() {
return Err(format!("solvecraft-cli commands failed:\n{}", String::from_utf8_lossy(&out.stderr)));
}
let cmds: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("commands JSON: {e}"))?;
let md = reference(cmds.as_array().map(Vec::as_slice).unwrap_or_default());
write(&book.join("src/reference/commands.md"), &md)?;
let gen_dir = book.join("src/generated");
std::fs::create_dir_all(&gen_dir).map_err(|e| format!("{}: {e}", gen_dir.display()))?;
let mut scripts = vec![root.join("examples/bracket.json")];
scripts.extend(json_files(&book.join("scripts"))?);
for s in &scripts {
let stem = s.file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or_default();
let png = gen_dir.join(format!("{stem}.png"));
let mut c = cli();
c.arg("snapshot").arg(s).arg("--out").arg(&png).args(["--width", SNAPSHOT_SIZE.0, "--height", SNAPSHOT_SIZE.1]);
let o = c.output().map_err(|e| format!("solvecraft-cli snapshot: {e}"))?;
if !o.status.success() {
return Err(format!("snapshot of {} failed:\n{}", s.display(), String::from_utf8_lossy(&o.stderr)));
}
println!("book: {}", png.strip_prefix(root).unwrap_or(&png).display());
}
let shot = "sample-plate.png";
std::fs::copy(root.join("docs/screenshots").join(shot), gen_dir.join(shot)).map_err(|e| format!("{shot}: {e}"))?;
if build {
let mut c = std::process::Command::new("mdbook");
c.arg("build").arg(&book);
crate::run(c, "mdbook build docs/book").map_err(|e| format!("{e}\n(install mdbook with `cargo install mdbook`)"))?;
println!("book: built into target/book");
}
Ok(())
}
/// The release solvecraft-cli just built.
fn cli() -> std::process::Command {
let mut c = cargo();
c.args(["run", "-q", "--release", "-p", "solidcraft-cli", "--"]);
c
}
fn write(path: &Path, text: &str) -> Result<(), String> {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
}
std::fs::write(path, text).map_err(|e| format!("{}: {e}", path.display()))
}
fn json_files(dir: &Path) -> Result<Vec<std::path::PathBuf>, String> {
let rd = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
let mut v: Vec<_> = rd.flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "json")).collect();
v.sort();
Ok(v)
}
/// The command reference page: one section per toolbar tab and panel, in registry order.
fn reference(cmds: &[Value]) -> String {
let s = |c: &Value, k: &str| c[k].as_str().unwrap_or_default().to_string();
let mut md = format!(
"# Command reference\n\nGenerated by `cargo xtask book` from `solvecraft-cli commands` ({} commands). Every command runs the same \
way from the toolbar, a script, `solvecraft-cli exec <id> '<json>'`, the control channel and MCP. Parameters marked `?` are \
optional; `expr` is a number or an expression with units.\n",
cmds.len()
);
let mut sections: Vec<(String, Vec<&Value>)> = Vec::new();
for tab in TABS {
for c in cmds.iter().filter(|c| s(c, "tab") == *tab) {
let title = format!("{tab} › {}", s(c, "panel"));
add(&mut sections, title, c);
}
}
for c in cmds.iter().filter(|c| !TABS.contains(&s(c, "tab").as_str())) {
let id = s(c, "id");
let prefix = id.split('.').next().unwrap_or_default().to_string();
add(&mut sections, format!("Other: {prefix}.*"), c);
}
for (title, list) in sections {
md.push_str(&format!("\n## {title}\n\n"));
for c in list {
let key = c["shortcut"].as_str().map(|k| format!(" ({k})")).unwrap_or_default();
md.push_str(&format!("- `{}` **{}**{key}", s(c, "id"), s(c, "label")));
let p = s(c, "params");
if !p.is_empty() {
md.push_str(&format!("<br>``{}``", p.replace('\n', " ")));
}
md.push('\n');
}
}
md
}
fn add<'a>(sections: &mut Vec<(String, Vec<&'a Value>)>, title: String, c: &'a Value) {
match sections.iter_mut().find(|(t, _)| *t == title) {
Some((_, v)) => v.push(c),
None => sections.push((title, vec![c])),
}
}