Skip to content

Commit dc27b52

Browse files
committed
Adjustments to oak_sources/
- Include `INDEX` in the cache - Add `PackageCache` trait - Add `struct TestPackageCache` with testing feature
1 parent 8f527fe commit dc27b52

6 files changed

Lines changed: 131 additions & 47 deletions

File tree

crates/oak_sources/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ serde.workspace = true
2020
serde_json.workspace = true
2121
sha2.workspace = true
2222
tar.workspace = true
23-
tempfile.workspace = true
23+
tempfile = { workspace = true, optional = true }
2424
ureq.workspace = true
2525

26+
[dev-dependencies]
27+
tempfile.workspace = true
28+
29+
[features]
30+
testing = ["dep:tempfile"]
31+
2632
[lints]
2733
workspace = true

crates/oak_sources/src/installed_package.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ pub(crate) struct InstalledPackage {
1313
}
1414

1515
impl InstalledPackage {
16-
pub(crate) fn find(package: &str, library_paths: &[PathBuf]) -> anyhow::Result<Option<Self>> {
16+
pub(crate) fn find<P: AsRef<Path>>(
17+
package: &str,
18+
library_paths: &[P],
19+
) -> anyhow::Result<Option<Self>> {
1720
let mut library_path = None;
1821

1922
for library_path_candidate in library_paths {
20-
if library_path_candidate.join(package).exists() {
21-
library_path = Some(library_path_candidate);
23+
if library_path_candidate.as_ref().join(package).exists() {
24+
library_path = Some(library_path_candidate.as_ref());
2225
break;
2326
}
2427
}
@@ -51,7 +54,7 @@ impl InstalledPackage {
5154
Ok(Some(Self {
5255
key,
5356
name: package.to_string(),
54-
library_path: library_path.clone(),
57+
library_path: library_path.to_path_buf(),
5558
description,
5659
description_hash,
5760
}))
@@ -92,6 +95,10 @@ impl InstalledPackage {
9295
self.package_path().join("NAMESPACE")
9396
}
9497

98+
pub(crate) fn index_path(&self) -> PathBuf {
99+
self.package_path().join("INDEX")
100+
}
101+
95102
pub(crate) fn description_hash(&self) -> &str {
96103
&self.description_hash
97104
}

crates/oak_sources/src/lib.rs

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ mod fs;
55
mod hash;
66
mod installed_package;
77
mod srcref;
8+
#[cfg(any(test, feature = "testing"))]
9+
pub mod test;
10+
pub mod traits;
811

912
use std::collections::HashSet;
13+
use std::path::Path;
1014
use std::path::PathBuf;
1115
use std::sync::RwLock;
1216

@@ -35,6 +39,12 @@ const METADATA_FILENAME: &str = ".metadata";
3539
/// the DESCRIPTION `Build:` timestamp being different).
3640
const ONE_WEEK: TimeDelta = TimeDelta::weeks(1);
3741

42+
impl crate::traits::PackageCache for PackageCache {
43+
fn get(&self, package: &str) -> Option<PathBuf> {
44+
self.get(package)
45+
}
46+
}
47+
3848
/// A cache of extracted R package sources
3949
///
4050
/// # On disk layout
@@ -109,12 +119,13 @@ const ONE_WEEK: TimeDelta = TimeDelta::weeks(1);
109119
///
110120
/// [`get`]: PackageCache::get
111121
/// [`clean`]: PackageCache::clean
122+
#[derive(Debug)]
112123
pub struct PackageCache {
113-
/// Path to `R` binary
124+
/// Path to an R executable
114125
r: PathBuf,
115126

116-
/// Set of R library paths
117-
r_libpaths: Vec<PathBuf>,
127+
/// Library paths to consider
128+
library_paths: Vec<PathBuf>,
118129

119130
/// On disk cache directory root
120131
cache_root: file_lock::Filesystem,
@@ -148,7 +159,7 @@ struct Metadata {
148159
}
149160

150161
impl PackageCache {
151-
pub fn new(r: PathBuf, r_libpaths: Vec<PathBuf>) -> anyhow::Result<Self> {
162+
pub fn new(r: PathBuf, library_paths: Vec<PathBuf>) -> anyhow::Result<Self> {
152163
let cache_root = file_lock::Filesystem::new(crate::fs::sources_dir()?);
153164
cache_root.create_dir()?;
154165

@@ -165,7 +176,7 @@ impl PackageCache {
165176

166177
Ok(Self {
167178
r,
168-
r_libpaths,
179+
library_paths,
169180
cache_root,
170181
cache_root_lock,
171182
source_unavailable: RwLock::new(HashSet::new()),
@@ -188,7 +199,7 @@ impl PackageCache {
188199
}
189200

190201
fn get_result(&self, package: &str) -> anyhow::Result<Option<PathBuf>> {
191-
let Some(package) = InstalledPackage::find(package, &self.r_libpaths)? else {
202+
let Some(package) = InstalledPackage::find(package, &self.library_paths)? else {
192203
// Not even installed
193204
return Ok(None);
194205
};
@@ -212,9 +223,9 @@ impl PackageCache {
212223
// Write path
213224
let result = if matches!(package.description().priority, Some(Priority::Base)) {
214225
// R version to download is the same as the base package version
215-
self.try_populate_base(&package.description().version)
226+
self.try_populate_base(&package.description().version, &self.library_paths)
216227
} else {
217-
self.try_populate(&package)
228+
self.try_populate(&package, &self.r, &self.library_paths)
218229
};
219230

220231
match result {
@@ -245,7 +256,11 @@ impl PackageCache {
245256
}
246257
}
247258

248-
fn try_populate_base(&self, version: &str) -> anyhow::Result<bool> {
259+
fn try_populate_base<P: AsRef<Path>>(
260+
&self,
261+
version: &str,
262+
library_paths: &[P],
263+
) -> anyhow::Result<bool> {
249264
// Download the R sources in their entirety
250265
let Some(bytes) = crate::base::download(version)? else {
251266
log::trace!("No R source tarball on CRAN for version {version}");
@@ -254,7 +269,7 @@ impl PackageCache {
254269

255270
// Populate all base packages from the download
256271
for package in crate::base::BASE_PACKAGES {
257-
let Some(package) = InstalledPackage::find(package, &self.r_libpaths)? else {
272+
let Some(package) = InstalledPackage::find(package, library_paths)? else {
258273
// It would be very odd to not find a base package
259274
return Ok(false);
260275
};
@@ -304,14 +319,24 @@ impl PackageCache {
304319
)?;
305320
}
306321

322+
crate::fs::copy_as_readonly(
323+
package.index_path(),
324+
destination_lock.parent().join("INDEX"),
325+
)?;
326+
307327
// Last! `.metadata` is the completion sentinel.
308328
self.write_metadata(package, &destination_lock)?;
309329

310330
Ok(())
311331
}
312332

313333
/// Writes `DESCRIPTION`, `NAMESPACE`, and `R/` to the cache entry, if possible
314-
fn try_populate(&self, package: &InstalledPackage) -> anyhow::Result<bool> {
334+
fn try_populate<P: AsRef<Path>, Q: AsRef<Path>>(
335+
&self,
336+
package: &InstalledPackage,
337+
r: P,
338+
library_paths: &[Q],
339+
) -> anyhow::Result<bool> {
315340
// Take per-key exclusive lock
316341
let destination = self.cache_root.join(package.key());
317342
destination.create_dir()?;
@@ -326,7 +351,7 @@ impl PackageCache {
326351
// writing `.metadata`.
327352
destination_lock.remove_siblings()?;
328353

329-
if !self.write_r_files(package, &destination_lock)? {
354+
if !self.write_r_files(package, r, library_paths, &destination_lock)? {
330355
return Ok(false);
331356
}
332357

@@ -338,25 +363,31 @@ impl PackageCache {
338363
package.namespace_path(),
339364
destination_lock.parent().join("NAMESPACE"),
340365
)?;
366+
crate::fs::copy_as_readonly(
367+
package.index_path(),
368+
destination_lock.parent().join("INDEX"),
369+
)?;
341370

342371
// Last! Only write `.metadata` if all other writes succeed. It is our completion sentinal.
343372
self.write_metadata(package, &destination_lock)?;
344373

345374
Ok(true)
346375
}
347376

348-
fn write_r_files(
377+
fn write_r_files<P: AsRef<Path>, Q: AsRef<Path>>(
349378
&self,
350379
package: &InstalledPackage,
380+
r: P,
381+
library_paths: &[Q],
351382
destination_lock: &FileLock,
352383
) -> anyhow::Result<bool> {
353384
// Try caching from srcref
354385
match crate::srcref::cache_srcref(
355386
package.name(),
356387
&package.description().version,
357388
destination_lock,
358-
&self.r,
359-
&self.r_libpaths,
389+
r,
390+
library_paths,
360391
) {
361392
Ok(true) => {
362393
log::trace!(
@@ -515,22 +546,3 @@ impl PackageCache {
515546
Ok(())
516547
}
517548
}
518-
519-
// // For local testing
520-
// #[cfg(test)]
521-
// mod tests {
522-
// use std::path::PathBuf;
523-
//
524-
// use crate::PackageCache;
525-
//
526-
// #[test]
527-
// fn testit() {
528-
// let r_script_path = PathBuf::from("/usr/local/bin/Rscript");
529-
// let r_libpaths = vec![
530-
// PathBuf::from("/Users/davis/Library/R/arm64/4.5/library"),
531-
// PathBuf::from("/Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library"),
532-
// ];
533-
// let cache = PackageCache::new(r_script_path, r_libpaths).unwrap();
534-
// cache.get("utils");
535-
// }
536-
// }

crates/oak_sources/src/srcref.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use std::path::Path;
2-
use std::path::PathBuf;
32

43
use oak_fs::file_lock::FileLock;
54

@@ -9,25 +8,25 @@ const SCRIPT: &str = include_str!("../scripts/srcrefs.R");
98
/// them to the cache at the parent folder containing `destination_lock`
109
///
1110
/// Launches a sidecar R session to read the srcrefs from the installed package.
12-
pub(crate) fn cache_srcref(
11+
pub(crate) fn cache_srcref<P: AsRef<Path>, Q: AsRef<Path>>(
1312
package: &str,
1413
version: &str,
1514
destination_lock: &FileLock,
16-
r: &Path,
17-
r_libpaths: &[PathBuf],
15+
r: P,
16+
library_paths: &[Q],
1817
) -> anyhow::Result<bool> {
1918
let args = &[package, version];
2019

2120
// Set `R_LIBS` to ensure the correct R package libraries are checked
2221
// `:` on Unix, `;` on Windows, see `?R_LIBS`
23-
let libpaths = r_libpaths
22+
let library_paths = library_paths
2423
.iter()
25-
.map(|libpath| libpath.to_string_lossy())
24+
.map(|library_path| library_path.as_ref().to_string_lossy())
2625
.collect::<Vec<_>>()
2726
.join(if cfg!(windows) { ";" } else { ":" });
28-
let env = &[("R_LIBS", libpaths.as_str())];
27+
let env = &[("R_LIBS", library_paths.as_str())];
2928

30-
let output = oak_r_process::run_text(r, SCRIPT, args, env)?;
29+
let output = oak_r_process::run_text(r.as_ref(), SCRIPT, args, env)?;
3130

3231
let code = output.status.code().unwrap_or(1);
3332

@@ -120,6 +119,8 @@ fn parse_line_directive(line: &str) -> Option<String> {
120119

121120
#[cfg(test)]
122121
mod tests {
122+
use std::path::PathBuf;
123+
123124
use oak_fs::file_lock::Filesystem;
124125

125126
use super::*;

crates/oak_sources/src/test.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use std::path::Path;
2+
use std::path::PathBuf;
3+
4+
use tempfile::TempDir;
5+
6+
impl crate::traits::PackageCache for TestPackageCache {
7+
fn get(&self, package: &str) -> Option<PathBuf> {
8+
self.get(package)
9+
}
10+
}
11+
12+
/// A fake package cache that can be used for testing
13+
#[derive(Debug)]
14+
pub struct TestPackageCache {
15+
root: TempDir,
16+
}
17+
18+
impl TestPackageCache {
19+
pub fn new() -> anyhow::Result<Self> {
20+
let root = TempDir::new()?;
21+
Ok(Self { root })
22+
}
23+
24+
pub fn get(&self, package: &str) -> Option<PathBuf> {
25+
let package = self.root.path().join(package);
26+
27+
if package.exists() {
28+
Some(package)
29+
} else {
30+
None
31+
}
32+
}
33+
34+
pub fn add(&self, package: &str, files: Vec<(&Path, &str)>) -> anyhow::Result<()> {
35+
let package = self.root.path().join(package);
36+
std::fs::create_dir(&package)?;
37+
38+
let r = package.join("R");
39+
std::fs::create_dir(&r)?;
40+
41+
for (name, content) in files {
42+
let path = r.join(name);
43+
std::fs::write(path, content)?;
44+
}
45+
46+
Ok(())
47+
}
48+
}

crates/oak_sources/src/traits.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use std::path::PathBuf;
2+
3+
/// Trait for the public API of any package cache
4+
///
5+
/// Implemented by the main [crate::PackageCache] itself, but also by
6+
/// [crate::test::TestPackageCache] so that you can generate a test cache that doesn't
7+
/// need internet access or access to a live R session.
8+
pub trait PackageCache: std::fmt::Debug + Sync + Send {
9+
fn get(&self, package: &str) -> Option<PathBuf>;
10+
}

0 commit comments

Comments
 (0)