1use std::{
2 env,
3 error::Error,
4 fs,
5 path::{Component, Path, PathBuf},
6 process::Command,
7};
8
9use flate2::read::GzDecoder;
10use sha2::{Digest, Sha256};
11use toml::{Table, Value, map::Map};
12
13mod edit;
14
15pub use edit::*;
16pub use ra_ap_syntax::ast;
17
18#[derive(Debug)]
19pub struct LockedPackage {
20 pub version: String,
21 pub checksum: String,
22 pub git_revision: Option<String>,
23}
24
25pub fn prepare_bridge_package(
26 package_name: &str,
27 generated_dir: &str,
28) -> Result<(PathBuf, LockedPackage), Box<dyn Error>> {
29 let manifest = bridge_manifest_path();
30 let (mut package, lock) = locked_package(package_name, &manifest)?;
31 let archive = registry_archive(package_name, &package, &manifest)?;
32 verify_archive_checksum(&archive, &package)?;
33
34 let generated =
35 PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")).join(generated_dir);
36 unpack_crate_archive(package_name, &archive, &generated, &package)?;
37 verify_manifest_matches_bridge(package_name, &generated.join("Cargo.toml"), &manifest)?;
38 package.git_revision = crate_git_revision(&generated)?;
39 rewrite_lib_header(&generated.join("src/lib.rs"))?;
40
41 println!("cargo:rerun-if-changed={}", archive.display());
42 println!("cargo:rerun-if-changed={}", manifest.display());
43 println!("cargo:rerun-if-changed={}", lock.display());
44
45 Ok((generated, package))
46}
47
48fn registry_archive(
49 package_name: &str,
50 package: &LockedPackage,
51 manifest: &Path,
52) -> Result<PathBuf, Box<dyn Error>> {
53 let cargo_home = cargo_home()?;
54 if let Some(archive) = find_registry_archive(package_name, &cargo_home, package)? {
55 return Ok(archive);
56 }
57
58 fetch_registry_packages(manifest)?;
59
60 if let Some(archive) = find_registry_archive(package_name, &cargo_home, package)? {
61 return Ok(archive);
62 }
63
64 Err(format!(
65 "could not find {} under {} after `cargo fetch --locked`",
66 archive_name(package_name, package),
67 cargo_home.join("registry").join("cache").display()
68 )
69 .into())
70}
71
72fn cargo_home() -> Result<PathBuf, Box<dyn Error>> {
73 env::var_os("CARGO_HOME")
74 .map(PathBuf::from)
75 .or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".cargo")))
76 .or_else(|| env::var_os("USERPROFILE").map(|home| PathBuf::from(home).join(".cargo")))
77 .ok_or_else(|| "CARGO_HOME is unavailable".into())
78}
79
80fn find_registry_archive(
81 package_name: &str,
82 cargo_home: &Path,
83 package: &LockedPackage,
84) -> Result<Option<PathBuf>, Box<dyn Error>> {
85 let registry_cache = cargo_home.join("registry").join("cache");
86 let package_archive = archive_name(package_name, package);
87 let Ok(registries) = fs::read_dir(®istry_cache) else {
88 return Ok(None);
89 };
90
91 for registry in registries {
92 let candidate = registry?.path().join(&package_archive);
93 if candidate.is_file() {
94 return Ok(Some(candidate));
95 }
96 }
97
98 Ok(None)
99}
100
101fn fetch_registry_packages(manifest: &Path) -> Result<(), Box<dyn Error>> {
102 let cargo = env::var_os("CARGO").ok_or("CARGO is unavailable")?;
103 let status = Command::new(cargo)
104 .arg("fetch")
105 .arg("--locked")
106 .arg("--manifest-path")
107 .arg(manifest)
108 .status()?;
109
110 if !status.success() {
111 return Err(format!("cargo fetch --locked failed with {status}").into());
112 }
113
114 Ok(())
115}
116
117fn verify_archive_checksum(archive: &Path, package: &LockedPackage) -> Result<(), Box<dyn Error>> {
118 let actual = hex_digest(fs::read(archive)?);
119
120 if actual != package.checksum {
121 return Err(format!(
122 "checksum mismatch for {}: expected {}, got {actual}",
123 archive.display(),
124 package.checksum,
125 )
126 .into());
127 }
128
129 Ok(())
130}
131
132fn locked_package(
133 package_name: &str,
134 manifest: &Path,
135) -> Result<(LockedPackage, PathBuf), Box<dyn Error>> {
136 let version = upstream_package_version(package_name, manifest)?;
137 let locks = lockfile_candidates(manifest);
138
139 for lock in locks.iter().filter(|lock| lock.is_file()) {
140 if let Some(checksum) = locked_package_checksum(package_name, lock, &version)? {
141 return Ok((
142 LockedPackage {
143 version: version.clone(),
144 checksum,
145 git_revision: None,
146 },
147 lock.clone(),
148 ));
149 }
150 }
151
152 Err(format!(
153 "could not find registry {package_name} {version} checksum in {}",
154 locks
155 .iter()
156 .map(|lock| lock.display().to_string())
157 .collect::<Vec<_>>()
158 .join(", ")
159 )
160 .into())
161}
162
163fn bridge_manifest_path() -> PathBuf {
164 PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"))
165 .join("Cargo.toml")
166}
167
168fn upstream_package_version(package_name: &str, manifest: &Path) -> Result<String, Box<dyn Error>> {
169 let manifest = read_manifest(manifest)?;
170 let dependencies = manifest_table(&manifest, &["target", "cfg(any())", "dependencies"])
171 .ok_or("bridge manifest has no cfg(any()) source dependency")?;
172 let version = dependencies
173 .iter()
174 .find(|(name, dependency)| dependency_package_name(name, dependency) == package_name)
175 .and_then(|(_, dependency)| dependency_version(dependency))
176 .ok_or_else(|| {
177 format!("bridge manifest has no cfg(any()) dependency for {package_name}")
178 })?;
179
180 exact_version(version)
181}
182
183fn dependency_package_name<'a>(name: &'a str, dependency: &'a Value) -> &'a str {
184 dependency
185 .as_table()
186 .and_then(|dependency| dependency.get("package"))
187 .and_then(Value::as_str)
188 .unwrap_or(name)
189}
190
191fn dependency_version(dependency: &Value) -> Option<&str> {
192 match dependency {
193 Value::String(version) => Some(version),
194 Value::Table(dependency) => dependency.get("version").and_then(Value::as_str),
195 _ => None,
196 }
197}
198
199fn exact_version(version: &str) -> Result<String, Box<dyn Error>> {
200 version
201 .strip_prefix('=')
202 .map(str::trim)
203 .filter(|version| !version.is_empty())
204 .map(str::to_owned)
205 .ok_or_else(|| {
206 format!("bridge source dependency version must be exact, got {version}").into()
207 })
208}
209
210fn lockfile_candidates(manifest: &Path) -> Vec<PathBuf> {
211 let manifest_dir = manifest
212 .parent()
213 .expect("manifest path has a parent directory");
214 let package_lock = manifest_dir.join("Cargo.lock");
215 let workspace_lock = manifest_dir.join("../..").join("Cargo.lock");
216 if package_lock == workspace_lock {
217 vec![package_lock]
218 } else {
219 vec![package_lock, workspace_lock]
220 }
221}
222
223fn crate_git_revision(generated: &Path) -> Result<Option<String>, Box<dyn Error>> {
224 let path = generated.join(".cargo_vcs_info.json");
225 let source = match fs::read_to_string(path) {
226 Ok(source) => source,
227 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
228 Err(error) => return Err(error.into()),
229 };
230 let info: serde_json::Value = serde_json::from_str(&source)?;
231 let revision = info
232 .get("git")
233 .and_then(|git| git.get("sha1"))
234 .and_then(serde_json::Value::as_str)
235 .ok_or(".cargo_vcs_info.json does not contain git.sha1")?;
236 if revision.len() != 40 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit()) {
237 return Err(format!("invalid git.sha1 in .cargo_vcs_info.json: {revision}").into());
238 }
239 Ok(Some(revision.to_ascii_lowercase()))
240}
241
242fn locked_package_checksum(
243 package_name: &str,
244 lock: &Path,
245 target_version: &str,
246) -> Result<Option<String>, Box<dyn Error>> {
247 let lock = fs::read_to_string(lock)?;
248 let mut name = None;
249 let mut version = None;
250 let mut source = None;
251 let mut checksum = None;
252
253 for line in lock.lines().chain(["[[package]]"]) {
254 if line == "[[package]]" {
255 if name.as_deref() == Some(package_name)
256 && version.as_deref() == Some(target_version)
257 && source
258 .as_deref()
259 .is_some_and(|source: &str| source.starts_with("registry+"))
260 {
261 return Ok(checksum);
262 }
263 name = None;
264 version = None;
265 source = None;
266 checksum = None;
267 continue;
268 }
269
270 if let Some(value) = line.strip_prefix("name = ") {
271 name = Some(value.trim_matches('"').to_owned());
272 } else if let Some(value) = line.strip_prefix("version = ") {
273 version = Some(value.trim_matches('"').to_owned());
274 } else if let Some(value) = line.strip_prefix("source = ") {
275 source = Some(value.trim_matches('"').to_owned());
276 } else if let Some(value) = line.strip_prefix("checksum = ") {
277 checksum = Some(value.trim_matches('"').to_owned());
278 }
279 }
280
281 Ok(None)
282}
283
284fn hex_digest(bytes: impl AsRef<[u8]>) -> String {
285 Sha256::digest(bytes)
286 .iter()
287 .map(|byte| format!("{byte:02x}"))
288 .collect()
289}
290
291fn unpack_crate_archive(
292 package_name: &str,
293 archive: &Path,
294 destination: &Path,
295 package: &LockedPackage,
296) -> Result<(), Box<dyn Error>> {
297 let decoder = GzDecoder::new(fs::File::open(archive)?);
298 let mut archive = tar::Archive::new(decoder);
299 let package_dir = package_dir(package_name, package);
300
301 for entry in archive.entries()? {
302 let mut entry = entry?;
303 let path = entry.path()?;
304 let relative = checked_archive_path(&path, &package_dir)?;
305 let destination_path = destination.join(relative);
306 let entry_type = entry.header().entry_type();
307
308 if entry_type.is_dir() {
309 fs::create_dir_all(&destination_path)?;
310 } else if entry_type.is_file() {
311 if let Some(parent) = destination_path.parent() {
312 fs::create_dir_all(parent)?;
313 }
314 entry.unpack(&destination_path)?;
315 } else {
316 return Err(format!("unsupported archive entry type for {}", path.display()).into());
317 }
318 }
319
320 Ok(())
321}
322
323fn rewrite_lib_header(lib_rs: &Path) -> Result<(), Box<dyn Error>> {
324 let source = fs::read_to_string(lib_rs)?;
325 let mut rewritten = String::new();
326
327 for line in source.lines() {
328 if line.starts_with("//!") || line.starts_with("#![") {
329 continue;
330 }
331
332 rewritten.push_str(line);
333 rewritten.push('\n');
334 }
335
336 fs::write(lib_rs, rewritten)?;
337 Ok(())
338}
339
340fn checked_archive_path(path: &Path, package_dir: &str) -> Result<PathBuf, Box<dyn Error>> {
341 let mut components = path.components();
342 match components.next() {
343 Some(Component::Normal(component)) if component == package_dir => {}
344 _ => return Err(format!("unexpected archive path {}", path.display()).into()),
345 }
346
347 let mut relative = PathBuf::new();
348 for component in components {
349 match component {
350 Component::Normal(part) => relative.push(part),
351 _ => return Err(format!("unsupported archive path {}", path.display()).into()),
352 }
353 }
354
355 if relative.as_os_str().is_empty() {
356 Ok(PathBuf::from("."))
357 } else {
358 Ok(relative)
359 }
360}
361
362fn verify_manifest_matches_bridge(
363 package_name: &str,
364 upstream_manifest_path: &Path,
365 bridge_manifest_path: &Path,
366) -> Result<(), Box<dyn Error>> {
367 let upstream_manifest = read_manifest(upstream_manifest_path)?;
368 let bridge_manifest = read_manifest(bridge_manifest_path)?;
369 let mut mismatches = Vec::new();
370
371 compare_manifest_section(
372 "dependencies",
373 normalized_dependencies(&upstream_manifest, None),
374 normalized_dependencies(
375 &bridge_manifest,
376 manifest_table(&upstream_manifest, &["dependencies"]),
377 ),
378 true,
379 &mut mismatches,
380 );
381 compare_manifest_section(
382 "target",
383 normalized_target_dependencies(&upstream_manifest, None),
384 normalized_target_dependencies(&bridge_manifest, Some(&upstream_manifest)),
385 true,
386 &mut mismatches,
387 );
388 compare_manifest_section(
389 "features",
390 manifest_section(&upstream_manifest, &["features"]),
391 manifest_section(&bridge_manifest, &["features"]),
392 false,
393 &mut mismatches,
394 );
395 compare_manifest_section(
396 "lints",
397 manifest_section(&upstream_manifest, &["lints"]),
398 manifest_section(&bridge_manifest, &["lints"]),
399 false,
400 &mut mismatches,
401 );
402
403 if mismatches.is_empty() {
404 Ok(())
405 } else {
406 Err(format!(
407 "bridge Cargo.toml is out of sync with {package_name}:\n{}",
408 mismatches.join("\n")
409 )
410 .into())
411 }
412}
413
414fn read_manifest(path: &Path) -> Result<Value, Box<dyn Error>> {
415 Ok(toml::from_str(&fs::read_to_string(path)?)?)
416}
417
418fn manifest_table<'a>(manifest: &'a Value, path: &[&str]) -> Option<&'a Table> {
419 let mut value = manifest;
420 for key in path {
421 value = value.get(*key)?;
422 }
423 value.as_table()
424}
425
426fn manifest_section(manifest: &Value, path: &[&str]) -> Option<Value> {
427 let mut value = manifest;
428 for key in path {
429 value = value.get(*key)?;
430 }
431 Some(value.clone())
432}
433
434fn compare_manifest_section(
435 label: &str,
436 expected: Option<Value>,
437 actual: Option<Value>,
438 allow_extra: bool,
439 mismatches: &mut Vec<String>,
440) {
441 match (expected, actual) {
442 (Some(expected), Some(actual)) if expected == actual => {}
443 (Some(Value::Table(expected)), Some(Value::Table(actual))) => {
444 compare_manifest_tables(label, &expected, &actual, allow_extra, mismatches);
445 }
446 (Some(_), Some(_)) => mismatches.push(format!(" {label}: different value")),
447 (Some(_), None) => {
448 mismatches.push(format!(" {label}: missing section in bridge manifest"))
449 }
450 (None, Some(_)) => mismatches.push(format!(" {label}: extra section in bridge manifest")),
451 (None, None) => {}
452 }
453}
454
455fn compare_manifest_tables(
456 label: &str,
457 expected: &Table,
458 actual: &Table,
459 allow_extra: bool,
460 mismatches: &mut Vec<String>,
461) {
462 let expected_only = table_keys(expected)
463 .into_iter()
464 .filter(|key| !actual.contains_key(key))
465 .collect::<Vec<_>>();
466 let actual_only = table_keys(actual)
467 .into_iter()
468 .filter(|key| !expected.contains_key(key))
469 .collect::<Vec<_>>();
470 let changed = table_keys(expected)
471 .into_iter()
472 .filter(|key| actual.get(key) != expected.get(key))
473 .collect::<Vec<_>>();
474
475 if !expected_only.is_empty() {
476 mismatches.push(format!(
477 " {label}: missing keys in bridge manifest: {}",
478 expected_only.join(", ")
479 ));
480 }
481 if !actual_only.is_empty() && !allow_extra {
482 mismatches.push(format!(
483 " {label}: extra keys in bridge manifest: {}",
484 actual_only.join(", ")
485 ));
486 }
487 if !changed.is_empty() {
488 mismatches.push(format!(
489 " {label}: different values: {}",
490 changed.join(", ")
491 ));
492 }
493}
494
495fn normalized_dependencies(manifest: &Value, reference: Option<&Table>) -> Option<Value> {
496 Some(Value::Table(normalize_dependencies(
497 manifest_table(manifest, &["dependencies"])?,
498 reference,
499 )))
500}
501
502fn normalized_target_dependencies(manifest: &Value, reference: Option<&Value>) -> Option<Value> {
503 let targets = manifest_table(manifest, &["target"])?;
504 let mut normalized_targets = Map::new();
505
506 for (target, target_value) in targets {
507 if target == "cfg(any())" {
508 continue;
509 }
510 let Some(target_table) = target_value.as_table() else {
511 continue;
512 };
513 let Some(dependencies) = target_table.get("dependencies").and_then(Value::as_table) else {
514 continue;
515 };
516 let reference_dependencies = reference.and_then(|reference| {
517 manifest_table(reference, &["target", target.as_str(), "dependencies"])
518 });
519 let mut normalized_target = Map::new();
520 normalized_target.insert(
521 "dependencies".to_owned(),
522 Value::Table(normalize_dependencies(dependencies, reference_dependencies)),
523 );
524 normalized_targets.insert(target.clone(), Value::Table(normalized_target));
525 }
526
527 if normalized_targets.is_empty() {
528 None
529 } else {
530 Some(Value::Table(normalized_targets))
531 }
532}
533
534fn normalize_dependencies(dependencies: &Table, reference: Option<&Table>) -> Table {
535 dependencies
536 .iter()
537 .map(|(name, value)| {
538 (
539 name.clone(),
540 normalize_dependency(value, reference.and_then(|reference| reference.get(name))),
541 )
542 })
543 .collect()
544}
545
546fn normalize_dependency(value: &Value, reference: Option<&Value>) -> Value {
547 let Some(mut dependency) = dependency_table(value) else {
548 return value.clone();
549 };
550 let Some(reference) = reference.and_then(dependency_table) else {
551 return Value::Table(dependency);
552 };
553
554 let had_path = dependency.remove("path").is_some();
555 if had_path || is_replaced_package_dependency(&dependency, &reference) {
556 copy_dependency_field(&mut dependency, &reference, "package");
557 copy_dependency_field(&mut dependency, &reference, "version");
558 }
559
560 Value::Table(dependency)
561}
562
563fn dependency_table(value: &Value) -> Option<Table> {
564 match value {
565 Value::String(version) => {
566 let mut dependency = Map::new();
567 dependency.insert("version".to_owned(), Value::String(version.clone()));
568 Some(dependency)
569 }
570 Value::Table(dependency) => Some(dependency.clone()),
571 _ => None,
572 }
573}
574
575fn is_replaced_package_dependency(dependency: &Table, reference: &Table) -> bool {
576 dependency.get("package").is_some()
577 && dependency.get("package") != reference.get("package")
578 && dependency.get("version") != reference.get("version")
579}
580
581fn copy_dependency_field(dependency: &mut Table, reference: &Table, field: &str) {
582 if let Some(value) = reference.get(field) {
583 dependency.insert(field.to_owned(), value.clone());
584 } else {
585 dependency.remove(field);
586 }
587}
588
589fn table_keys(table: &Table) -> Vec<String> {
590 let mut keys = table.keys().cloned().collect::<Vec<_>>();
591 keys.sort();
592 keys
593}
594
595fn package_dir(package_name: &str, package: &LockedPackage) -> String {
596 format!("{package_name}-{}", package.version)
597}
598
599fn archive_name(package_name: &str, package: &LockedPackage) -> String {
600 format!("{}.crate", package_dir(package_name, package))
601}