From a971a14898b0e05b01015bddce769e75976f5168 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 21:30:40 -0500 Subject: [PATCH] Fix Flatpak updates and bump to 0.24.1 (#6649) * fix(flatpak): allow release checks and self-update helpers * test: keep Flatpak permission parsing within strict Clippy rules * chore: bump version to 0.24.1 for the Flatpak update fix * test: locate CLI binaries after Nextest archive relocation * docs: explain the Flatpak update permission bootstrap --- Cargo.lock | 60 +++++++-------- Cargo.toml | 2 +- apps/vectorcraft-cli/tests/it/cli.rs | 73 ++++++++++--------- apps/vectorcraft-cli/tests/it/fileio.rs | 10 +-- apps/vectorcraft-cli/tests/it/main.rs | 8 ++ apps/vectorcraft-cli/tests/it/mcp_e2e.rs | 4 +- apps/vectorcraft-cli/tests/it/script.rs | 4 +- apps/vectorcraft/tests/it/automation_args.rs | 11 ++- apps/vectorcraft/tests/it/help.rs | 4 +- apps/vectorcraft/tests/it/main.rs | 8 ++ crates/ui-egui/tests/flatpak_updates.rs | 27 +++++++ docs/releasing.md | 6 ++ .../ai.storyteller.vectorcraft.bundle.yml | 2 + .../flatpak/ai.storyteller.vectorcraft.yml | 2 + 14 files changed, 135 insertions(+), 86 deletions(-) create mode 100644 crates/ui-egui/tests/flatpak_updates.rs diff --git a/Cargo.lock b/Cargo.lock index 920a735ab..5c2bdbdbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6313,7 +6313,7 @@ dependencies = [ [[package]] name = "vectorcraft" -version = "0.24.0" +version = "0.24.1" dependencies = [ "arboard", "block2 0.6.2", @@ -6360,7 +6360,7 @@ dependencies = [ [[package]] name = "vectorcraft-affinity" -version = "0.24.0" +version = "0.24.1" dependencies = [ "crc32fast", "flate2", @@ -6371,14 +6371,14 @@ dependencies = [ [[package]] name = "vectorcraft-apischema" -version = "0.24.0" +version = "0.24.1" dependencies = [ "serde_json", ] [[package]] name = "vectorcraft-brush" -version = "0.24.0" +version = "0.24.1" dependencies = [ "kurbo 0.13.1", "serde", @@ -6390,7 +6390,7 @@ dependencies = [ [[package]] name = "vectorcraft-cad" -version = "0.24.0" +version = "0.24.1" dependencies = [ "image", "kurbo 0.13.1", @@ -6406,7 +6406,7 @@ dependencies = [ [[package]] name = "vectorcraft-cli" -version = "0.24.0" +version = "0.24.1" dependencies = [ "log", "objc2-core-foundation", @@ -6429,7 +6429,7 @@ dependencies = [ [[package]] name = "vectorcraft-color" -version = "0.24.0" +version = "0.24.1" dependencies = [ "kurbo 0.13.1", "moxcms", @@ -6441,7 +6441,7 @@ dependencies = [ [[package]] name = "vectorcraft-doc" -version = "0.24.0" +version = "0.24.1" dependencies = [ "chrono", "hayro-ccitt", @@ -6460,7 +6460,7 @@ dependencies = [ [[package]] name = "vectorcraft-effects" -version = "0.24.0" +version = "0.24.1" dependencies = [ "image", "kurbo 0.13.1", @@ -6478,7 +6478,7 @@ dependencies = [ [[package]] name = "vectorcraft-engine" -version = "0.24.0" +version = "0.24.1" dependencies = [ "cfb", "chrono", @@ -6531,7 +6531,7 @@ dependencies = [ [[package]] name = "vectorcraft-eps" -version = "0.24.0" +version = "0.24.1" dependencies = [ "craft-pdf", "encoding_rs", @@ -6559,7 +6559,7 @@ dependencies = [ [[package]] name = "vectorcraft-format" -version = "0.24.0" +version = "0.24.1" dependencies = [ "craft-vectorcraft-format", "flate2", @@ -6575,7 +6575,7 @@ dependencies = [ [[package]] name = "vectorcraft-geom" -version = "0.24.0" +version = "0.24.1" dependencies = [ "kurbo 0.13.1", "proptest", @@ -6587,7 +6587,7 @@ dependencies = [ [[package]] name = "vectorcraft-js" -version = "0.24.0" +version = "0.24.1" dependencies = [ "chrono", "getrandom 0.4.3", @@ -6597,7 +6597,7 @@ dependencies = [ [[package]] name = "vectorcraft-mcp" -version = "0.24.0" +version = "0.24.1" dependencies = [ "image", "log", @@ -6616,7 +6616,7 @@ dependencies = [ [[package]] name = "vectorcraft-metafile" -version = "0.24.0" +version = "0.24.1" dependencies = [ "image", "kurbo 0.13.1", @@ -6630,7 +6630,7 @@ dependencies = [ [[package]] name = "vectorcraft-pathops" -version = "0.24.0" +version = "0.24.1" dependencies = [ "kurbo 0.13.1", "linesweeper", @@ -6642,7 +6642,7 @@ dependencies = [ [[package]] name = "vectorcraft-pdf" -version = "0.24.0" +version = "0.24.1" dependencies = [ "aes", "chrono", @@ -6676,7 +6676,7 @@ dependencies = [ [[package]] name = "vectorcraft-plugins" -version = "0.24.0" +version = "0.24.1" dependencies = [ "craft-vectorcraft-format", "image", @@ -6695,7 +6695,7 @@ dependencies = [ [[package]] name = "vectorcraft-render" -version = "0.24.0" +version = "0.24.1" dependencies = [ "flate2", "gif 0.14.2", @@ -6725,7 +6725,7 @@ dependencies = [ [[package]] name = "vectorcraft-scriptcheck" -version = "0.24.0" +version = "0.24.1" dependencies = [ "proptest", "unicode-properties", @@ -6733,7 +6733,7 @@ dependencies = [ [[package]] name = "vectorcraft-svg" -version = "0.24.0" +version = "0.24.1" dependencies = [ "craft-docpath", "flate2", @@ -6760,7 +6760,7 @@ dependencies = [ [[package]] name = "vectorcraft-testkit" -version = "0.24.0" +version = "0.24.1" dependencies = [ "fax", "image", @@ -6778,7 +6778,7 @@ dependencies = [ [[package]] name = "vectorcraft-text" -version = "0.24.0" +version = "0.24.1" dependencies = [ "harfrust 0.13.3", "icu_segmenter", @@ -6799,7 +6799,7 @@ dependencies = [ [[package]] name = "vectorcraft-three-d" -version = "0.24.0" +version = "0.24.1" dependencies = [ "vectorcraft-geom", "vectorcraft-pathops", @@ -6807,7 +6807,7 @@ dependencies = [ [[package]] name = "vectorcraft-tools" -version = "0.24.0" +version = "0.24.1" dependencies = [ "image", "kurbo 0.13.1", @@ -6824,7 +6824,7 @@ dependencies = [ [[package]] name = "vectorcraft-trace" -version = "0.24.0" +version = "0.24.1" dependencies = [ "image", "kurbo 0.13.1", @@ -6836,7 +6836,7 @@ dependencies = [ [[package]] name = "vectorcraft-ui-egui" -version = "0.24.0" +version = "0.24.1" dependencies = [ "bytemuck", "craft-keymap", @@ -6874,7 +6874,7 @@ dependencies = [ [[package]] name = "vectorcraft-web" -version = "0.24.0" +version = "0.24.1" dependencies = [ "craft-web-api", "eframe", @@ -8006,7 +8006,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee" [[package]] name = "xtask" -version = "0.24.0" +version = "0.24.1" dependencies = [ "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 606c34f8b..b733a0f3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"] default-members = ["crates/*", "apps/vectorcraft", "apps/vectorcraft-cli", "xtask"] [workspace.package] -version = "0.24.0" +version = "0.24.1" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.95" diff --git a/apps/vectorcraft-cli/tests/it/cli.rs b/apps/vectorcraft-cli/tests/it/cli.rs index 5bdd2bf63..0b1c56090 100644 --- a/apps/vectorcraft-cli/tests/it/cli.rs +++ b/apps/vectorcraft-cli/tests/it/cli.rs @@ -3,8 +3,6 @@ use std::process::{Command, Stdio}; use serde_json::{Value, json}; -const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft-cli"); - /// Returns the path of `name` in `vectorcraft-cli-test-` in the temp folder. The first call removes /// that folder and creates it again empty. fn tmp(name: &str) -> std::path::PathBuf { @@ -27,7 +25,12 @@ fn approval_param_needs_the_start_up_flag(key: &str, value: Value) { std::fs::write(&steps, json!([{"command": "plugin.install", "params": {"dataBase64": module, key: value}}]).to_string()).unwrap(); let config = empty_dir(std::env::temp_dir().join(format!("vectorcraft-cli-test-{key}-{}", std::process::id()))); let run = |extra: &[&str]| { - Command::new(BIN).env("VECTORCRAFT_CONFIG_DIR", &config).args(["run", "--commands", steps.to_str().unwrap()]).args(extra).output().unwrap() + Command::new(crate::bin()) + .env("VECTORCRAFT_CONFIG_DIR", &config) + .args(["run", "--commands", steps.to_str().unwrap()]) + .args(extra) + .output() + .unwrap() }; let out = run(&[]); assert!(!out.status.success()); @@ -38,7 +41,7 @@ fn approval_param_needs_the_start_up_flag(key: &str, value: Value) { let mcp_config = empty_dir(std::env::temp_dir().join(format!("vectorcraft-cli-test-mcp-{key}-{}", std::process::id()))); let mcp_install = |extra: &[&str]| -> Value { - let mut child = Command::new(BIN) + let mut child = Command::new(crate::bin()) .env("VECTORCRAFT_CONFIG_DIR", &mcp_config) .args(["mcp", "--headless"]) .args(extra) @@ -77,22 +80,23 @@ fn plugin_grants_need_the_start_up_flag() { approval_param_needs_the_start_up_flag("grant", json!(["doc.read"])); approval_param_needs_the_start_up_flag("allowHeavy", json!(true)); - let out = Command::new(BIN).args(["mcp", "--allow-plugin-grants", "--connect", "127.0.0.1:1"]).output().unwrap(); + let out = Command::new(crate::bin()).args(["mcp", "--allow-plugin-grants", "--connect", "127.0.0.1:1"]).output().unwrap(); assert!(!out.status.success()); assert!(String::from_utf8_lossy(&out.stderr).contains("--allow-plugin-grants applies to the headless server")); let root = std::env::temp_dir(); for flag in ["--automation-read-root", "--automation-write-root"] { - let out = Command::new(BIN).args(["mcp", "--allow-plugin-grants", flag, root.to_str().unwrap()]).stdin(Stdio::null()).output().unwrap(); + let out = + Command::new(crate::bin()).args(["mcp", "--allow-plugin-grants", flag, root.to_str().unwrap()]).stdin(Stdio::null()).output().unwrap(); assert!(!out.status.success(), "{flag}"); assert!(String::from_utf8_lossy(&out.stderr).contains("--allow-plugin-grants can't be used with"), "{flag}"); } - let usage = Command::new(BIN).arg("help").output().unwrap(); + let usage = Command::new(crate::bin()).arg("help").output().unwrap(); assert!(String::from_utf8_lossy(&usage.stdout).contains("--allow-plugin-grants lets plugin.install's grant and allowHeavy take effect")); } #[test] fn commands_prints_catalogue() { - let out = Command::new(BIN).arg("commands").output().unwrap(); + let out = Command::new(crate::bin()).arg("commands").output().unwrap(); assert!(out.status.success()); let v: Value = serde_json::from_slice(&out.stdout).unwrap(); let ids: Vec<&str> = v.as_array().unwrap().iter().filter_map(|c| c["id"].as_str()).collect(); @@ -104,7 +108,7 @@ fn run_batch_exports() { let svg = tmp("batch.svg"); let png = tmp("batch.png"); let dc = tmp("batch.vectorcraft"); - let out = Command::new(BIN) + let out = Command::new(crate::bin()) .args(["run", "--cmd", "file.new", "--params", r#"{"width":200,"height":100}"#]) .args(["--cmd", "shape.ellipse", "--params", r#"{"x":10,"y":10,"width":80,"height":60}"#]) .args(["--cmd", "paint.setFill", "--params", r##"{"color":"#3366ff"}"##]) @@ -121,7 +125,7 @@ fn run_batch_exports() { assert_eq!(u32::from_be_bytes(png_bytes[16..20].try_into().unwrap()), 400); // Re-open the native file and inspect. - let out = Command::new(BIN).args(["run", "--in", dc.to_str().unwrap(), "--cmd", "document.inspect"]).output().unwrap(); + let out = Command::new(crate::bin()).args(["run", "--in", dc.to_str().unwrap(), "--cmd", "document.inspect"]).output().unwrap(); assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); let last: Value = serde_json::from_str(String::from_utf8(out.stdout).unwrap().lines().last().unwrap()).unwrap(); assert_eq!(last["result"]["artboards"][0]["width"], 200.0); @@ -137,7 +141,7 @@ fn scale_sizes_raster_exports_only() { let svg = svg.to_str().unwrap(); let (plain, scaled, png) = (tmp("plain.dxf"), tmp("scaled.dxf"), tmp("scaled.png")); let ok = |args: &[&str]| { - let out = Command::new(BIN).args(args).output().unwrap(); + let out = Command::new(crate::bin()).args(args).output().unwrap(); assert!(out.status.success(), "{args:?}: {}", String::from_utf8_lossy(&out.stderr)); }; ok(&["convert", svg, plain.to_str().unwrap()]); @@ -151,11 +155,11 @@ fn scale_sizes_raster_exports_only() { #[test] fn run_reports_errors() { - let out = Command::new(BIN).args(["run", "--cmd", "nope.nothing"]).output().unwrap(); + let out = Command::new(crate::bin()).args(["run", "--cmd", "nope.nothing"]).output().unwrap(); assert!(!out.status.success()); let stderr = String::from_utf8_lossy(&out.stderr).to_string(); assert!(stderr.contains("step 0 (nope.nothing)"), "{stderr}"); - let out = Command::new(BIN).args(["run", "--params", "{}"]).output().unwrap(); + let out = Command::new(crate::bin()).args(["run", "--params", "{}"]).output().unwrap(); assert!(!out.status.success()); } @@ -184,7 +188,7 @@ fn run_reads_steps_from_a_commands_file() { {"id": "object.setProps", "params": {"id": "$2.ids.0", "name": "barcode"}}, ]); std::fs::write(&steps, serde_json::to_string_pretty(&file).unwrap()).unwrap(); - let out = Command::new(BIN) + let out = Command::new(crate::bin()) .args(["run", "--cmd", "file.new", "--params", r#"{"width":200,"height":100}"#, "--commands", steps.to_str().unwrap()]) .args(["--cmd", "document.node", "--params", r#"{"id":"$2.ids.0"}"#, "--export", svg.to_str().unwrap()]) .output() @@ -204,7 +208,7 @@ fn run_reads_steps_from_a_commands_file() { #[test] fn run_reads_json_lines_from_standard_input() { let svg = tmp("stdin.svg"); - let mut child = Command::new(BIN) + let mut child = Command::new(crate::bin()) .args(["run", "--commands", "-", "--export", svg.to_str().unwrap()]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -227,7 +231,7 @@ fn run_reads_json_lines_from_standard_input() { #[test] fn run_refuses_malformed_commands_naming_the_step() { let fails = |args: &[&str]| { - let out = Command::new(BIN).args(args).stdin(Stdio::null()).output().unwrap(); + let out = Command::new(crate::bin()).args(args).stdin(Stdio::null()).output().unwrap(); assert!(!out.status.success(), "{args:?}"); (String::from_utf8_lossy(&out.stderr).to_string(), json_lines(&out.stdout).len()) }; @@ -275,11 +279,11 @@ fn run_refuses_malformed_commands_naming_the_step() { /// `--help` and `-h` right after a subcommand print the usage that `vectorcraft-cli help` prints. #[test] fn help_after_a_subcommand_prints_the_usage() { - let usage = Command::new(BIN).arg("help").output().unwrap(); + let usage = Command::new(crate::bin()).arg("help").output().unwrap(); assert!(usage.status.success() && String::from_utf8_lossy(&usage.stdout).contains("USAGE:")); for sub in ["mcp", "run", "commands", "convert", "info", "bench", "perf"] { for flag in ["--help", "-h"] { - let out = Command::new(BIN).args([sub, flag]).stdin(Stdio::null()).output().unwrap(); + let out = Command::new(crate::bin()).args([sub, flag]).stdin(Stdio::null()).output().unwrap(); assert!(out.status.success(), "{sub} {flag}: {}", String::from_utf8_lossy(&out.stderr)); assert!(out.stdout == usage.stdout, "{sub} {flag}: {}", String::from_utf8_lossy(&out.stdout)); } @@ -290,7 +294,7 @@ fn help_after_a_subcommand_prints_the_usage() { /// with them. #[test] fn help_later_in_the_line_prints_the_usage() { - let usage = Command::new(BIN).arg("help").output().unwrap(); + let usage = Command::new(crate::bin()).arg("help").output().unwrap(); let mut wrong = vec![]; for args in [ &["mcp", "--headless", "--help"][..], @@ -311,7 +315,7 @@ fn help_later_in_the_line_prints_the_usage() { &["perf", "--paths", "many", "-h"], &["convert", "--decimals", "many", "--help"], ] { - let out = Command::new(BIN).args(args).stdin(Stdio::null()).output().unwrap(); + let out = Command::new(crate::bin()).args(args).stdin(Stdio::null()).output().unwrap(); if !out.status.success() || out.stdout != usage.stdout { wrong.push(format!("{args:?}: {}{}", String::from_utf8_lossy(&out.stderr), String::from_utf8_lossy(&out.stdout))); } @@ -322,17 +326,18 @@ fn help_later_in_the_line_prints_the_usage() { /// Further along the line, `-h` is the value of the option before it. #[test] fn help_as_an_option_value_goes_to_the_option() { - let out = Command::new(BIN).args(["run", "--cmd", "-h"]).output().unwrap(); + let out = Command::new(crate::bin()).args(["run", "--cmd", "-h"]).output().unwrap(); let err = String::from_utf8_lossy(&out.stderr); assert!(!out.status.success() && err.contains("vectorcraft-cli: step 0 (-h):") && out.stdout.is_empty(), "{err}"); - let out = Command::new(BIN).args(["convert", "in.svg", "--range", "-h"]).output().unwrap(); + let out = Command::new(crate::bin()).args(["convert", "in.svg", "--range", "-h"]).output().unwrap(); let err = String::from_utf8_lossy(&out.stderr); assert!(!out.status.success() && err.contains("convert needs IN and OUT") && out.stdout.is_empty(), "{err}"); } #[test] fn mcp_headless_over_stdio() { - let mut child = Command::new(BIN).args(["mcp", "--headless"]).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().unwrap(); + let mut child = + Command::new(crate::bin()).args(["mcp", "--headless"]).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().unwrap(); { let mut stdin = child.stdin.take().unwrap(); for m in [ @@ -359,13 +364,13 @@ fn mcp_headless_over_stdio() { /// plug-ins. #[test] fn safe_mode_mcp_does_not_connect_to_a_running_app() { - let out = Command::new(BIN).args(["--safe-mode", "mcp", "--connect", "127.0.0.1:9"]).env("VECTORCRAFT_NO_PREFS", "1").output().unwrap(); + let out = Command::new(crate::bin()).args(["--safe-mode", "mcp", "--connect", "127.0.0.1:9"]).env("VECTORCRAFT_NO_PREFS", "1").output().unwrap(); let err = String::from_utf8_lossy(&out.stderr); assert!(!out.status.success() && err.contains("--safe-mode runs a headless session"), "{err}"); // Something listening on the default address, as a running app does; a running app or another // program already holding it serves as well. let _app = std::net::TcpListener::bind(vectorcraft_mcp::DEFAULT_ADDR); - let out = Command::new(BIN).args(["--safe-mode", "mcp"]).env("VECTORCRAFT_NO_PREFS", "1").stdin(Stdio::null()).output().unwrap(); + let out = Command::new(crate::bin()).args(["--safe-mode", "mcp"]).env("VECTORCRAFT_NO_PREFS", "1").stdin(Stdio::null()).output().unwrap(); let err = String::from_utf8_lossy(&out.stderr); assert!(out.status.success() && err.contains("MCP server on stdio (headless)"), "{err}"); } @@ -402,7 +407,7 @@ fn mcp_automation_roots_take_several_folders() { } let dir = |d: &str| base.join(d).to_string_lossy().into_owned(); let file = |d: &str, f: &str| base.join(d).join(f).to_string_lossy().into_owned(); - let mut child = Command::new(BIN) + let mut child = Command::new(crate::bin()) .args(["mcp", "--automation-read-root", &dir("in"), &format!("--automation-read-root={}", dir("in2"))]) .args(["--automation-write-root", &dir("out"), "--automation-write-root", &dir("out2")]) .stdin(Stdio::piped()) @@ -457,7 +462,7 @@ fn automation_roots(base: &std::path::Path) { } let dir = |d: &str| base.join(d).to_string_lossy().into_owned(); let fails = |args: &[&str], why: &str| { - let out = Command::new(BIN).args(args).stdin(Stdio::null()).output().unwrap(); + let out = Command::new(crate::bin()).args(args).stdin(Stdio::null()).output().unwrap(); let err = String::from_utf8_lossy(&out.stderr); assert!(!out.status.success() && err.contains(why), "{args:?}: {err}"); }; @@ -469,7 +474,7 @@ fn automation_roots(base: &std::path::Path) { "a running app enforces its own roots: start it with `vectorcraft --control ", ); - let mut child = Command::new(BIN) + let mut child = Command::new(crate::bin()) .args(["mcp", "--automation-read-root", &dir("in"), &format!("--automation-write-root={}", dir("out"))]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -530,7 +535,7 @@ fn automation_roots(base: &std::path::Path) { fn mcp_protocol_over_stdio() { use std::io::{BufRead, BufReader}; let mut child = - Command::new(BIN).args(["mcp", "--headless"]).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap(); + Command::new(crate::bin()).args(["mcp", "--headless"]).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap(); let mut stdin = child.stdin.take().unwrap(); let mut lines = BufReader::new(child.stdout.take().unwrap()).lines(); let mut notes = vec![]; @@ -711,7 +716,7 @@ fn run_command(id: u64, command: &str, params: Value) -> Value { #[test] fn closed_stdout_ends_quietly() { use std::io::Read; - let mut child = Command::new(BIN).arg("commands").stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap(); + let mut child = Command::new(crate::bin()).arg("commands").stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap(); let mut first = [0u8; 16]; child.stdout.as_mut().unwrap().read_exact(&mut first).unwrap(); drop(child.stdout.take()); @@ -728,13 +733,13 @@ fn run_with_a_closed_stdout_ends_quietly_and_finishes_the_batch() { let _ = std::fs::remove_file(&svg); let rect = r#"{"x":1,"y":1,"width":5,"height":5}"#; let args = ["run", "--cmd", "document.json", "--cmd", "shape.rectangle", "--params", rect, "--export", svg.to_str().unwrap()]; - let mut child = Command::new(BIN).args(args).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap(); + let mut child = Command::new(crate::bin()).args(args).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap(); drop(child.stdout.take()); let out = child.wait_with_output().unwrap(); let err = String::from_utf8_lossy(&out.stderr); assert!(out.status.success() && err.is_empty(), "{:?}: {err}", out.status); assert!(svg.is_file(), "the export after the closed reader still ran"); - let out = Command::new(BIN).args(["run", "--cmd", "not-a-command"]).stdout(Stdio::null()).output().unwrap(); + let out = Command::new(crate::bin()).args(["run", "--cmd", "not-a-command"]).stdout(Stdio::null()).output().unwrap(); assert!(!out.status.success()); } @@ -749,7 +754,7 @@ fn config_dir(name: &str) -> std::path::PathBuf { /// One `vectorcraft-cli run` with `steps` ((command, params)) against the settings folder `config`; /// the result of each step. fn run_in(config: &std::path::Path, steps: &[(&str, Value)]) -> Vec { - let mut cmd = Command::new(BIN); + let mut cmd = Command::new(crate::bin()); cmd.arg("run").env("VECTORCRAFT_CONFIG_DIR", config).env_remove("VECTORCRAFT_NO_PREFS"); for (id, params) in steps { cmd.args(["--cmd", id, "--params", ¶ms.to_string()]); @@ -820,7 +825,7 @@ fn saved_pdf_presets_and_loaded_profiles_survive_a_restart() { #[test] fn runs_without_preferences_remember_nothing() { let config = config_dir("noprefs"); - let out = Command::new(BIN) + let out = Command::new(crate::bin()) .args(["run", "--cmd", "pdf.preset.save", "--params", r#"{"name":"Press"}"#]) .env("VECTORCRAFT_CONFIG_DIR", &config) .env("VECTORCRAFT_NO_PREFS", "1") diff --git a/apps/vectorcraft-cli/tests/it/fileio.rs b/apps/vectorcraft-cli/tests/it/fileio.rs index c60357b5f..71044be4a 100644 --- a/apps/vectorcraft-cli/tests/it/fileio.rs +++ b/apps/vectorcraft-cli/tests/it/fileio.rs @@ -4,8 +4,6 @@ use std::process::Command; use serde_json::Value; -const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft-cli"); - /// Returns the path of `name` in `vectorcraft-cli-fileio-` in the temp folder. The first call /// removes that folder and creates it again empty. fn tmp(name: &str) -> String { @@ -20,7 +18,7 @@ fn tmp(name: &str) -> String { } fn ok(args: &[&str]) -> String { - let out = Command::new(BIN).args(args).output().unwrap(); + let out = Command::new(crate::bin()).args(args).output().unwrap(); assert!(out.status.success(), "{args:?}: {}", String::from_utf8_lossy(&out.stderr)); String::from_utf8(out.stdout).unwrap() } @@ -130,7 +128,7 @@ fn raster_exports_use_document_resolution_unless_overridden() { fn raster_export_rejects_invalid_ppi() { for ppi in ["0", "-150", "NaN", "inf", "high"] { for args in [vec!["run", "--export", "unused.png", "--ppi", ppi], vec!["convert", "unused.svg", "unused.png", "--ppi", ppi]] { - let out = Command::new(BIN).args(&args).output().unwrap(); + let out = Command::new(crate::bin()).args(&args).output().unwrap(); assert!(!out.status.success(), "{args:?}"); assert!(String::from_utf8_lossy(&out.stderr).contains("positive finite number"), "{args:?}: {out:?}"); } @@ -228,7 +226,7 @@ fn convert_writes_legacy_ai_with_its_options() { assert!(r["warnings"].to_string().contains("hidden objects are left out"), "{r}"); assert!(std::fs::read(&one).unwrap().starts_with(b"%!PS-Adobe-3.0")); // A value the engine doesn't take is an error. - let bad = Command::new(BIN).args(["convert", &src, &tmp("bad.ai"), "--ai-version", "7", "--hidden-objects", "behind"]).output().unwrap(); + let bad = Command::new(crate::bin()).args(["convert", &src, &tmp("bad.ai"), "--ai-version", "7", "--hidden-objects", "behind"]).output().unwrap(); assert!(!bad.status.success()); assert!(String::from_utf8_lossy(&bad.stderr).contains("hiddenObjects"), "{}", String::from_utf8_lossy(&bad.stderr)); } @@ -253,7 +251,7 @@ fn convert_and_run_write_ait_templates() { assert_eq!(info(ait)["artboards"].as_array().unwrap().len(), 2, "{ait}"); } // A template holds every artboard: one can't be picked. - let one = Command::new(BIN).args(["convert", &pdf, &tmp("one.ait"), "--artboard", "0"]).output().unwrap(); + let one = Command::new(crate::bin()).args(["convert", &pdf, &tmp("one.ait"), "--artboard", "0"]).output().unwrap(); assert!(!one.status.success()); assert!(String::from_utf8_lossy(&one.stderr).contains("every artboard"), "{}", String::from_utf8_lossy(&one.stderr)); } diff --git a/apps/vectorcraft-cli/tests/it/main.rs b/apps/vectorcraft-cli/tests/it/main.rs index 07e5a3a70..d898dda20 100644 --- a/apps/vectorcraft-cli/tests/it/main.rs +++ b/apps/vectorcraft-cli/tests/it/main.rs @@ -6,3 +6,11 @@ mod cli; mod fileio; mod mcp_e2e; mod script; + +/// Nextest relocates archived binaries; Cargo's compile-time path still names the build runner. +fn bin() -> std::path::PathBuf { + std::env::var_os("NEXTEST_BIN_EXE_vectorcraft_cli") + .or_else(|| std::env::var_os("CARGO_BIN_EXE_vectorcraft-cli")) + .map(std::path::PathBuf::from) + .unwrap_or_else(|| env!("CARGO_BIN_EXE_vectorcraft-cli").into()) +} diff --git a/apps/vectorcraft-cli/tests/it/mcp_e2e.rs b/apps/vectorcraft-cli/tests/it/mcp_e2e.rs index 5a353070a..4931602ec 100644 --- a/apps/vectorcraft-cli/tests/it/mcp_e2e.rs +++ b/apps/vectorcraft-cli/tests/it/mcp_e2e.rs @@ -6,8 +6,6 @@ use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; use serde_json::{Value, json}; use vectorcraft_testkit::raster::Image; -const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft-cli"); - struct Mcp { child: Child, stdin: Option, @@ -17,7 +15,7 @@ struct Mcp { impl Mcp { fn spawn() -> Self { - let mut child = Command::new(BIN) + let mut child = Command::new(crate::bin()) .args(["mcp", "--headless"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/apps/vectorcraft-cli/tests/it/script.rs b/apps/vectorcraft-cli/tests/it/script.rs index d68716f7b..3652e585d 100644 --- a/apps/vectorcraft-cli/tests/it/script.rs +++ b/apps/vectorcraft-cli/tests/it/script.rs @@ -2,11 +2,9 @@ use std::process::Command; use serde_json::{Value, json}; -const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft-cli"); - /// `run` with `script.run` after `file.new`, with `params`. fn run_script(params: &Value) -> std::process::Output { - Command::new(BIN).args(["run", "--cmd", "file.new", "--cmd", "script.run", "--params", ¶ms.to_string()]).output().unwrap() + Command::new(crate::bin()).args(["run", "--cmd", "file.new", "--cmd", "script.run", "--params", ¶ms.to_string()]).output().unwrap() } /// `run` runs a script headless: the reply holds the report, with what the script logged and the diff --git a/apps/vectorcraft/tests/it/automation_args.rs b/apps/vectorcraft/tests/it/automation_args.rs index 93b33fa16..4a1e6f524 100644 --- a/apps/vectorcraft/tests/it/automation_args.rs +++ b/apps/vectorcraft/tests/it/automation_args.rs @@ -1,8 +1,6 @@ use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; -const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft"); - /// Invalid roots must exit before desktop initialization on every platform, including a CI /// machine with no display. A regression must fail rather than hang with a window open. fn rejects(args: &[&str], message: &str) { @@ -10,7 +8,7 @@ fn rejects(args: &[&str], message: &str) { } fn rejects_with_env(args: &[&str], env: &[(&str, &str)], message: &str) { - let mut child = Command::new(BIN) + let mut child = Command::new(crate::bin()) .args(args) .env_remove("VECTORCRAFT_CONTROL_PORT") .env_remove("VECTORCRAFT_AUTOMATION_READ_ROOT") @@ -62,7 +60,7 @@ fn missing_empty_or_option_operands_fail_before_startup() { #[test] fn repeated_roots_are_accepted_for_both_spellings() { for flag in ["--automation-read-root", "--automation-write-root"] { - let out = Command::new(BIN).args([flag, ".", format!("{flag}=.").as_str(), flag, "./", "--version"]).output().unwrap(); + let out = Command::new(crate::bin()).args([flag, ".", format!("{flag}=.").as_str(), flag, "./", "--version"]).output().unwrap(); assert!(out.status.success(), "{flag}: {}", String::from_utf8_lossy(&out.stderr)); assert_eq!(String::from_utf8(out.stdout).unwrap(), format!("vectorcraft {}\n", env!("CARGO_PKG_VERSION"))); assert!(out.stderr.is_empty()); @@ -73,7 +71,8 @@ fn repeated_roots_are_accepted_for_both_spellings() { #[test] fn explicit_dash_folder_does_not_consume_version() { - let out = Command::new(BIN).args(["--automation-read-root=-folder", "--automation-write-root", "./-folder", "--version"]).output().unwrap(); + let out = + Command::new(crate::bin()).args(["--automation-read-root=-folder", "--automation-write-root", "./-folder", "--version"]).output().unwrap(); assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); assert_eq!(String::from_utf8(out.stdout).unwrap(), format!("vectorcraft {}\n", env!("CARGO_PKG_VERSION"))); assert!(out.stderr.is_empty()); @@ -95,7 +94,7 @@ fn control_without_a_valid_port_fails_before_startup() { #[test] fn version_wins_over_a_bad_control_port_variable() { - let out = Command::new(BIN).arg("--version").env("VECTORCRAFT_CONTROL_PORT", "abc").output().unwrap(); + let out = Command::new(crate::bin()).arg("--version").env("VECTORCRAFT_CONTROL_PORT", "abc").output().unwrap(); assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); assert_eq!(String::from_utf8(out.stdout).unwrap(), format!("vectorcraft {}\n", env!("CARGO_PKG_VERSION"))); } diff --git a/apps/vectorcraft/tests/it/help.rs b/apps/vectorcraft/tests/it/help.rs index ece66b98a..d82475b9f 100644 --- a/apps/vectorcraft/tests/it/help.rs +++ b/apps/vectorcraft/tests/it/help.rs @@ -6,12 +6,10 @@ use std::process::{Command, Output, Stdio}; use std::sync::mpsc; use std::time::{Duration, Instant}; -const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft"); - /// Runs the desktop binary without a display and with a control port it cannot parse, so a /// regression exits with that error instead of opening a window. fn run(args: &[&str], env: &[(&str, &str)]) -> Output { - let mut child = Command::new(BIN) + let mut child = Command::new(crate::bin()) .args(args) .env_remove("DISPLAY") .env_remove("WAYLAND_DISPLAY") diff --git a/apps/vectorcraft/tests/it/main.rs b/apps/vectorcraft/tests/it/main.rs index d9dcf39cb..658f4d49f 100644 --- a/apps/vectorcraft/tests/it/main.rs +++ b/apps/vectorcraft/tests/it/main.rs @@ -4,3 +4,11 @@ mod automation_args; mod help; + +/// Nextest relocates archived binaries; Cargo's compile-time path still names the build runner. +fn bin() -> std::path::PathBuf { + std::env::var_os("NEXTEST_BIN_EXE_vectorcraft") + .or_else(|| std::env::var_os("CARGO_BIN_EXE_vectorcraft")) + .map(std::path::PathBuf::from) + .unwrap_or_else(|| env!("CARGO_BIN_EXE_vectorcraft").into()) +} diff --git a/crates/ui-egui/tests/flatpak_updates.rs b/crates/ui-egui/tests/flatpak_updates.rs new file mode 100644 index 000000000..c0ffb69fd --- /dev/null +++ b/crates/ui-egui/tests/flatpak_updates.rs @@ -0,0 +1,27 @@ +//! Release and source Flatpaks must allow release checks and host update helpers. +fn finish_args(manifest: &str) -> Vec<&str> { + manifest + .split("finish-args:") + .nth(1) + .unwrap_or_default() + .lines() + .take_while(|line| line.is_empty() || line.starts_with(' ') || line.starts_with('#')) + .filter_map(|line| line.trim().strip_prefix("- ")) + .map(|arg| arg.trim_matches(['\'', '"'])) + .collect() +} + +#[test] +fn flatpak_update_permissions() { + let manifests = [ + include_str!("../../../packaging/linux/flatpak/ai.storyteller.vectorcraft.bundle.yml"), + include_str!("../../../packaging/linux/flatpak/ai.storyteller.vectorcraft.yml"), + ]; + let args = manifests.map(finish_args); + assert_eq!(args[0], args[1], "source and release permissions must agree"); + for permissions in args { + for required in ["--share=network", "--talk-name=org.freedesktop.Flatpak"] { + assert!(permissions.contains(&required), "missing update permission: {required}"); + } + } +} diff --git a/docs/releasing.md b/docs/releasing.md index 64c177618..9de6abb3b 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -42,6 +42,12 @@ pick that branch for a full run. Run on any other branch, it is a dry run: the L FreeBSD and web jobs build and upload their artifacts, the environment refuses the signing jobs, and no draft release is created. +Flatpak permission repairs need one update through the existing host-side installation method. +After that update, Help > Check for Updates can inspect and update standalone bundles. +Repository-managed installations continue to use their package manager; launcher-pinned +versions keep their selected version. Both Flatpak manifests must grant network access, +the host Flatpak interface, and read-only access to the shared version choices. + ## What gets built | Platform | Artifacts | Built on | diff --git a/packaging/linux/flatpak/ai.storyteller.vectorcraft.bundle.yml b/packaging/linux/flatpak/ai.storyteller.vectorcraft.bundle.yml index 840abbc53..b3b4ad194 100644 --- a/packaging/linux/flatpak/ai.storyteller.vectorcraft.bundle.yml +++ b/packaging/linux/flatpak/ai.storyteller.vectorcraft.bundle.yml @@ -17,6 +17,8 @@ runtime-version: '25.08' sdk: org.freedesktop.Sdk command: vectorcraft finish-args: + # The shared updater inspects and updates this installation via flatpak-spawn --host. + - --talk-name=org.freedesktop.Flatpak - --filesystem=xdg-data/ArtCraft/versions:ro # X11 shared memory (MIT-SHM). Without it the X11 fallback is slow or fails. - --share=ipc diff --git a/packaging/linux/flatpak/ai.storyteller.vectorcraft.yml b/packaging/linux/flatpak/ai.storyteller.vectorcraft.yml index 681763793..7d14dbe01 100644 --- a/packaging/linux/flatpak/ai.storyteller.vectorcraft.yml +++ b/packaging/linux/flatpak/ai.storyteller.vectorcraft.yml @@ -17,6 +17,8 @@ sdk-extensions: - org.freedesktop.Sdk.Extension.rust-stable command: vectorcraft finish-args: + # The shared updater inspects and updates this installation via flatpak-spawn --host. + - --talk-name=org.freedesktop.Flatpak - --filesystem=xdg-data/ArtCraft/versions:ro # Keep identical to ai.storyteller.vectorcraft.bundle.yml (the release bundle); comments there. - --share=ipc