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}