Skip to content

Commit d0eac9c

Browse files
authored
(3) Extract out oak_srcref for srcref rebuilding (#1300)
Part of #1234 Branched from #1299 Not much to see here, exact same srcref mechanism as before. Just extracted into its own crate.
2 parents e6309d9 + 09a15ac commit d0eac9c

12 files changed

Lines changed: 568 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ oak_scan = { path = "crates/oak_scan" }
8787
oak_semantic = { path = "crates/oak_semantic" }
8888
oak_source = { path = "crates/oak_source" }
8989
oak_sources = { path = "crates/oak_sources" }
90+
oak_srcref = { path = "crates/oak_srcref" }
9091
once_cell = "1.21.4"
9192
parking_lot = "0.12.5"
9293
paste = "1.0.15"

crates/ark/src/lsp/diagnostics.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,6 +1610,7 @@ foo
16101610
imports: vec![],
16111611
repository: None,
16121612
priority: None,
1613+
built: None,
16131614
fields: Dcf::new(),
16141615
};
16151616
let package = Package::from_parts(PathBuf::from("/mock/path"), description, namespace);
@@ -1705,6 +1706,7 @@ foo
17051706
imports: vec![],
17061707
repository: None,
17071708
priority: None,
1709+
built: None,
17081710
fields: Dcf::new(),
17091711
};
17101712
let package1 =
@@ -1723,6 +1725,7 @@ foo
17231725
imports: vec![],
17241726
repository: None,
17251727
priority: None,
1728+
built: None,
17261729
fields: Dcf::new(),
17271730
};
17281731
let package2 =
@@ -1782,6 +1785,7 @@ foo
17821785
imports: vec![],
17831786
repository: None,
17841787
priority: None,
1788+
built: None,
17851789
fields: Dcf::new(),
17861790
};
17871791
let package = Package::from_parts(PathBuf::from("/mock/path"), description, namespace);

crates/ark/src/lsp/sources.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub(crate) trait SourceHandler: Send + Sync {
2323
pub(crate) struct SourceRequest {
2424
name: String,
2525
version: String,
26+
built: String,
2627
library_path: PathBuf,
2728
}
2829

@@ -139,6 +140,13 @@ impl SourceRequest {
139140
));
140141
};
141142

143+
let Some(built) = package.built(db).to_owned() else {
144+
// Only ever runs on installed packages, which always carry a `Built:` field
145+
return Err(anyhow::anyhow!(
146+
"Package {name} is missing a `Built` field to provide sources for."
147+
));
148+
};
149+
142150
let library_path = match package.description_path(db) {
143151
FilePath::File(path) => {
144152
match path.as_path().as_std_path().parent().and_then(Path::parent) {
@@ -160,6 +168,7 @@ impl SourceRequest {
160168
Ok(Self {
161169
name,
162170
version,
171+
built,
163172
library_path,
164173
})
165174
}
@@ -176,6 +185,12 @@ impl SourceRequest {
176185
&self.version
177186
}
178187

188+
// TODO!: Remove when we have a production `SourceHandler`
189+
#[cfg_attr(not(test), expect(dead_code))]
190+
pub(crate) fn built(&self) -> &str {
191+
&self.built
192+
}
193+
179194
// TODO!: Remove when we have a production `SourceHandler`
180195
#[cfg_attr(not(test), expect(dead_code))]
181196
pub(crate) fn library_path(&self) -> &Path {

crates/ark/src/lsp/tests/sources.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ async fn test_source_pipeline_ingests_package_sources() {
165165
assert_eq!(recorded.len(), 1);
166166
assert_eq!(recorded[0].name(), "donor");
167167
assert_eq!(recorded[0].version(), "0.0.0");
168+
assert_eq!(recorded[0].built(), "dummy");
168169
assert_eq!(recorded[0].library_path(), lib.path());
169170
}
170171

crates/ark/src/lsp/tests/utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub(super) fn write_description(dir: &Path, name: &str) {
4646
std::fs::create_dir_all(dir).unwrap();
4747
std::fs::write(
4848
dir.join("DESCRIPTION"),
49-
format!("Package: {name}\nVersion: 0.0.0\n"),
49+
format!("Package: {name}\nVersion: 0.0.0\nBuilt: dummy\n"),
5050
)
5151
.unwrap();
5252
}

crates/oak_db/src/package.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,17 @@ impl Package {
140140
.map(|description| description.version.clone())
141141
}
142142

143+
/// The package's `Built:`, parsed lazily from `DESCRIPTION`. `None` when
144+
/// the file is missing or has no `Built:` field (only installed packages have one).
145+
///
146+
/// Build timestamps change on every install, so backdating probably isn't that
147+
/// important here, so we don't track this method.
148+
pub fn built(self, db: &dyn Db) -> Option<String> {
149+
self.description(db)
150+
.as_ref()
151+
.and_then(|description| description.built.clone())
152+
}
153+
143154
/// The basename order from `DESCRIPTION`'s `Collate:` field, parsed
144155
/// lazily. `None` when the field (or the file) is absent. Narrow query
145156
/// over [`Package::description`], same backdating story as

crates/oak_package_metadata/src/description.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ pub struct Description {
1616

1717
pub priority: Option<Priority>,
1818

19+
/// Only present for installed packages
20+
pub built: Option<String>,
21+
1922
/// Raw DCF fields
2023
pub fields: Dcf,
2124
}
@@ -81,13 +84,16 @@ impl Description {
8184
None
8285
});
8386

87+
let built = fields.get("Built").map(str::to_string);
88+
8489
Ok(Description {
8590
name,
8691
version,
8792
depends,
8893
imports,
8994
repository,
9095
priority,
96+
built,
9197
fields,
9298
})
9399
}
@@ -232,6 +238,23 @@ Version: 1.0.0"#;
232238
assert_eq!(parsed.collate(), None);
233239
}
234240

241+
#[test]
242+
fn parses_description_with_built() {
243+
let desc = r#"Package: mypackage
244+
Version: 1.0.0
245+
Built: R 4.5.0; ; 2025-01-15 12:34:56 UTC; unix"#;
246+
let parsed = Description::parse(desc).unwrap();
247+
assert_eq!(
248+
parsed.built.as_deref(),
249+
Some("R 4.5.0; ; 2025-01-15 12:34:56 UTC; unix")
250+
);
251+
252+
let desc = r#"Package: mypackage
253+
Version: 1.0.0"#;
254+
let parsed = Description::parse(desc).unwrap();
255+
assert_eq!(parsed.built, None);
256+
}
257+
235258
#[test]
236259
fn parses_description_with_unknown_repository() {
237260
let desc = r#"Package: mypackage

crates/oak_srcref/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "oak_srcref"
3+
version = "0.1.0"
4+
authors.workspace = true
5+
edition.workspace = true
6+
rust-version.workspace = true
7+
license.workspace = true
8+
9+
[dependencies]
10+
anyhow.workspace = true
11+
hex.workspace = true
12+
log.workspace = true
13+
oak_cache.workspace = true
14+
oak_fs.workspace = true
15+
oak_r_process.workspace = true
16+
sha2.workspace = true
17+
18+
[dev-dependencies]
19+
tempfile.workspace = true
20+
21+
[lints]
22+
workspace = true
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Extract source code from an installed R package using srcref metadata.
2+
#
3+
# See also `lobstr::src()`:
4+
# https://github.com/r-lib/lobstr/blob/85dd18d43e4612f98b05bb93019a7b12fb0bbf6b/R/src.R#L205
5+
#
6+
# Arguments (via commandArgs):
7+
# 1. package - Package name
8+
# 2. version - Expected package version
9+
#
10+
# Library paths have been set ahead of time via the `R_LIBS` environment
11+
# variable.
12+
#
13+
# On success: prints the concatenated source lines (with `#line` directives) to
14+
# stdout.
15+
#
16+
# On failure: exits with code 1 (package unexpectedly not installed, version
17+
# mismatch) or 2 (no srcrefs).
18+
19+
args <- commandArgs(trailingOnly = TRUE)
20+
21+
if (length(args) != 2L) {
22+
message("'package' and 'version' are required arguments.")
23+
quit(status = 1L)
24+
}
25+
26+
package <- args[[1L]]
27+
version <- args[[2L]]
28+
29+
# Make sure package is installed
30+
path <- find.package(package, quiet = TRUE)
31+
if (length(path) == 0L) {
32+
message(paste0("Package '", package, "' is not installed"))
33+
quit(status = 1L)
34+
}
35+
36+
# Make sure installed version matches the requested version.
37+
# Normalize both to `package_version`s to handle versions like sf's 1.1-1.
38+
version <- as.character(package_version(version))
39+
installed_version <- as.character(packageVersion(package))
40+
if (installed_version != version) {
41+
message(paste0(
42+
"Version mismatch for '",
43+
package,
44+
"': ",
45+
"installed ",
46+
installed_version,
47+
", requested ",
48+
version
49+
))
50+
quit(status = 1L)
51+
}
52+
53+
# Get the package namespace and find a function object.
54+
# Functions will contain the srcrefs if srcrefs have been kept, and are what we
55+
# can back out the full file structure from.
56+
ns <- asNamespace(package)
57+
exports <- getNamespaceExports(ns)
58+
59+
fn <- NULL
60+
for (name in exports) {
61+
candidate <- get0(name, envir = ns, mode = "function")
62+
if (!is.null(candidate)) {
63+
fn <- candidate
64+
break
65+
}
66+
}
67+
68+
if (is.null(fn)) {
69+
message(paste0("No functions found for package '", package, "'"))
70+
quit(status = 2L)
71+
}
72+
73+
extract_lines <- function(fn) {
74+
srcref <- attr(fn, "srcref")
75+
if (is.null(srcref)) {
76+
return(NULL)
77+
}
78+
79+
srcfile <- attr(srcref, "srcfile")
80+
if (is.null(srcfile)) {
81+
return(NULL)
82+
}
83+
84+
original <- srcfile$original
85+
if (is.null(original)) {
86+
return(NULL)
87+
}
88+
89+
lines <- original$lines
90+
if (is.null(lines)) {
91+
return(NULL)
92+
}
93+
94+
lines
95+
}
96+
97+
lines <- extract_lines(fn)
98+
99+
if (is.null(lines)) {
100+
message(paste0("No srcrefs found for package '", package, "'"))
101+
quit(status = 2L)
102+
}
103+
104+
cat(lines, sep = "\n")

0 commit comments

Comments
 (0)