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/// These directories must exist before Kobo builds that generate compile-time
53/// bundled asset metadata, otherwise the generated list will be incomplete.
54///
55/// Workspace-level directories that already exist are left untouched.
56///
57/// # Errors
58///
59/// Returns an error if the GitHub API request fails, the download fails, or
60/// extraction fails.
61pub fn run() -> Result<()> {
62 let root = workspace::root()?;
63 let cache_dir = root.join(format!(".cache/plato-assets/{PLATO_VERSION}"));
64
65 let missing: Vec<&str> = ASSET_DIRS
66 .iter()
67 .copied()
68 .filter(|dir| !root.join(dir).exists())
69 .collect();
70
71 if missing.is_empty() {
72 println!("All asset directories already present, skipping download.");
73 return Ok(());
74 }
75
76 if cache_dir.exists() && all_dirs_cached(&cache_dir) {
77 println!("Restoring assets from cache (.cache/plato-assets/{PLATO_VERSION})…");
78 copy_from_cache(&cache_dir, &root, &missing)?;
79 return Ok(());
80 }
81
82 let asset_name = format!("plato-{PLATO_VERSION}.zip");
83 let asset = github::fetch_release_asset(PLATO_REPO, PLATO_VERSION, &asset_name)?;
84
85 println!("Downloading {asset_name} from Plato {PLATO_VERSION}…");
86
87 let archive = root.join(&asset_name);
88 github::download_asset(&asset, &archive).context("failed to download Plato release archive")?;
89
90 std::fs::create_dir_all(&cache_dir).context("failed to create plato-assets cache directory")?;
91
92 fs::extract_zip_paths(&archive, &cache_dir, ASSET_DIRS)
93 .context("failed to extract asset directories from Plato release archive")?;
94
95 std::fs::remove_file(&archive).ok();
96
97 copy_from_cache(&cache_dir, &root, &missing)?;
98
99 for dir in &missing {
100 println!("Extracted {dir}/");
101 }
102
103 Ok(())
104}
105
106/// Returns `true` if every asset directory is present in `cache_dir`.
107fn all_dirs_cached(cache_dir: &Path) -> bool {
108 ASSET_DIRS.iter().all(|dir| cache_dir.join(dir).exists())
109}
110
111/// Copies asset directories from `cache_dir` into `dest` for each name in `dirs`.
112fn copy_from_cache(cache_dir: &Path, dest: &Path, dirs: &[&str]) -> Result<()> {
113 for dir in dirs {
114 let src = cache_dir.join(dir);
115 let dst = dest.join(dir);
116 fs::copy_dir_all(&src, &dst)
117 .with_context(|| format!("failed to copy {dir}/ from cache to workspace"))?;
118 println!("Restored {dir}/ from cache");
119 }
120 Ok(())
121}