xtask_lib/tasks/download_assets.rs
1//! `cargo xtask download-assets` — download static assets from the Plato release.
2//!
3//! The Cadmus distribution requires several directories of static assets
4//! (`bin/`, `resources/`, `hyphenation-patterns/`) that are not stored in the
5//! repository and are not included in the Cadmus release artifact.
6//!
7//! # Temporary workaround
8//!
9//! These assets are sourced from the upstream [Plato] release zip until
10//! [issue #64] is resolved. Once Cadmus builds these directories from source
11//! as part of `cargo xtask build-kobo`, this entire task becomes unnecessary
12//! and should be removed along with the CI step that calls it.
13//!
14//! [Plato]: https://github.com/baskerville/plato
15//! [issue #64]: https://github.com/OGKevin/cadmus/issues/64
16//!
17//! ## Caching
18//!
19//! Extracted asset directories are cached under `.cache/plato-assets/<version>/`
20//! so that CI can restore them with a version-keyed cache and avoid re-downloading
21//! the zip on every run. The workspace-level directories (`bin/`, `resources/`,
22//! `hyphenation-patterns/`) are populated by copying from the cache directory.
23
24use std::path::Path;
25
26use anyhow::{Context, Result};
27
28use super::util::{fs, github, workspace};
29
30/// The Plato GitHub repository in `owner/name` format.
31///
32/// Asset directories are sourced from here until issue #64 is resolved.
33const PLATO_REPO: &str = "baskerville/plato";
34
35/// The pinned Plato release version.
36///
37/// Tracked by Renovate via a regex manager in `renovate.json`. Update this
38/// constant when a new Plato release is available.
39pub const PLATO_VERSION: &str = "0.9.45";
40
41/// Directories extracted from the Plato release zip into the workspace root.
42const ASSET_DIRS: &[&str] = &["bin", "resources", "hyphenation-patterns"];
43
44/// Downloads static asset directories from the pinned Plato release.
45///
46/// Checks `.cache/plato-assets/<version>/` first. If the cache directory
47/// already contains all required asset directories, they are copied from there
48/// without hitting the network. Otherwise the release zip is downloaded,
49/// the directories are extracted into the cache, and then copied to the
50/// workspace root.
51///
52/// Workspace-level directories that already exist are left untouched.
53///
54/// # Errors
55///
56/// Returns an error if the GitHub API request fails, the download fails, or
57/// extraction fails.
58pub fn run() -> Result<()> {
59 let root = workspace::root()?;
60 let cache_dir = root.join(format!(".cache/plato-assets/{PLATO_VERSION}"));
61
62 let missing: Vec<&str> = ASSET_DIRS
63 .iter()
64 .copied()
65 .filter(|dir| !root.join(dir).exists())
66 .collect();
67
68 if missing.is_empty() {
69 println!("All asset directories already present, skipping download.");
70 return Ok(());
71 }
72
73 if cache_dir.exists() && all_dirs_cached(&cache_dir) {
74 println!("Restoring assets from cache (.cache/plato-assets/{PLATO_VERSION})…");
75 copy_from_cache(&cache_dir, &root, &missing)?;
76 return Ok(());
77 }
78
79 let asset_name = format!("plato-{PLATO_VERSION}.zip");
80 let asset = github::fetch_release_asset(PLATO_REPO, PLATO_VERSION, &asset_name)?;
81
82 println!("Downloading {asset_name} from Plato {PLATO_VERSION}…");
83
84 let archive = root.join(&asset_name);
85 github::download_asset(&asset, &archive).context("failed to download Plato release archive")?;
86
87 std::fs::create_dir_all(&cache_dir).context("failed to create plato-assets cache directory")?;
88
89 fs::extract_zip_paths(&archive, &cache_dir, ASSET_DIRS)
90 .context("failed to extract asset directories from Plato release archive")?;
91
92 std::fs::remove_file(&archive).ok();
93
94 copy_from_cache(&cache_dir, &root, &missing)?;
95
96 for dir in &missing {
97 println!("Extracted {dir}/");
98 }
99
100 Ok(())
101}
102
103/// Returns `true` if every asset directory is present in `cache_dir`.
104fn all_dirs_cached(cache_dir: &Path) -> bool {
105 ASSET_DIRS.iter().all(|dir| cache_dir.join(dir).exists())
106}
107
108/// Copies asset directories from `cache_dir` into `dest` for each name in `dirs`.
109fn copy_from_cache(cache_dir: &Path, dest: &Path, dirs: &[&str]) -> Result<()> {
110 for dir in dirs {
111 let src = cache_dir.join(dir);
112 let dst = dest.join(dir);
113 fs::copy_dir_all(&src, &dst)
114 .with_context(|| format!("failed to copy {dir}/ from cache to workspace"))?;
115 println!("Restored {dir}/ from cache");
116 }
117 Ok(())
118}