Skip to main content

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}