1use crate::{
5 commands::{self, find, install},
6 subprocess::spack::SpackInvocation,
7};
8
9use std::{fs, io, path::Path};
10
11pub fn safe_create_dir_all_ioerror(path: &Path) -> Result<(), io::Error> {
14 match fs::create_dir(path) {
15 Ok(()) => return Ok(()),
16 Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => return Ok(()),
17 Err(ref e) if e.kind() == io::ErrorKind::NotFound => {},
18 Err(e) => return Err(e),
19 }
20 match path.parent() {
21 Some(p) => safe_create_dir_all_ioerror(p)?,
22 None => return Ok(()),
23 }
24 match fs::create_dir(path) {
25 Ok(()) => Ok(()),
26 Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()),
27 Err(e) => Err(e),
28 }
29}
30
31pub async fn ensure_installed(
37 spack: SpackInvocation,
38 spec: commands::CLISpec,
39) -> Result<find::FoundSpec, crate::Error> {
40 let install = install::Install {
41 spack: spack.clone(),
42 spec,
43 verbosity: Default::default(),
44 env: None,
45 repos: None,
46 };
47 let found_spec = install
48 .clone()
49 .install_find()
50 .await
51 .map_err(|e| commands::CommandError::Install(install, e))?;
52 Ok(found_spec)
53}
54
55pub async fn ensure_prefix(
59 spack: SpackInvocation,
60 spec: commands::CLISpec,
61) -> Result<prefix::Prefix, crate::Error> {
62 let found_spec = ensure_installed(spack.clone(), spec).await?;
63 let find_prefix = find::FindPrefix {
64 spack,
65 spec: found_spec.hashed_spec(),
66 env: None,
67 repos: None,
68 };
69 let prefix = find_prefix
70 .clone()
71 .find_prefix()
72 .await
73 .map_err(|e| commands::CommandError::FindPrefix(find_prefix, e))?
74 .expect("when using a spec with a hash, FindPrefix should never return None");
75 Ok(prefix)
76}
77
78pub mod wasm {
83 use crate::commands::find;
84
85 const LLVM_FOR_WASM: &str = "llvm@14:\
86+lld+clang+multiple-definitions\
87~compiler-rt~tools-extra-clang~libcxx~gold~openmp~internal_unwind~polly \
88targets=webassembly";
89
90 pub async fn ensure_wasm_ready_llvm(
93 spack: crate::SpackInvocation,
94 ) -> Result<find::FoundSpec, crate::Error> {
95 let llvm_found_spec = super::ensure_installed(spack, LLVM_FOR_WASM.into()).await?;
96 Ok(llvm_found_spec)
97 }
98}
99
100#[cfg(test)]
101mod test {
102 use tokio;
103
104 #[tokio::test]
134 async fn test_ensure_prefix() -> Result<(), crate::Error> {
135 use crate::{utils, SpackInvocation};
136 use super_process::{exe, fs, sync::SyncInvocable};
137
138 let spack = SpackInvocation::summon().await?;
140
141 let zip_prefix = utils::ensure_prefix(spack, "zip".into()).await?;
143 let zip_bin_path = zip_prefix.path.join("bin").join("zip");
144
145 let command = exe::Command {
147 exe: exe::Exe(fs::File(zip_bin_path)),
148 argv: ["--version"].as_ref().into(),
149 ..Default::default()
150 };
151 let output = command
152 .clone()
153 .invoke()
154 .await
155 .expect("expected zip command to succeed");
156 assert!(output.decode(command).unwrap().stdout.contains("\nThis is Zip "));
157 Ok(())
158 }
159
160 #[tokio::test]
161 async fn test_ensure_installed() -> Result<(), crate::Error> {
162 let spack = crate::SpackInvocation::summon().await?;
164
165 let zlib_spec = crate::utils::ensure_installed(spack, "zlib".into()).await?;
167 assert!(&zlib_spec.name == "zlib");
168 Ok(())
169 }
170}
171
172pub mod prefix {
173 use async_stream::try_stream;
174 use displaydoc::Display;
175 use futures_core::stream::Stream;
176 use futures_util::{pin_mut, stream::TryStreamExt};
177 use indexmap::{IndexMap, IndexSet};
178 use once_cell::sync::Lazy;
179 use regex::Regex;
180 use thiserror::Error;
181 use walkdir;
182
183 use std::path::{Path, PathBuf};
184
185 #[derive(Debug, Display, Error)]
186 pub enum PrefixTraversalError {
187 Walkdir(#[from] walkdir::Error),
189 NeededLibrariesNotFound(IndexSet<LibraryName>, Prefix, IndexSet<LibraryName>),
191 DuplicateLibraryNames(
193 IndexSet<LibraryName>,
194 Prefix,
195 IndexMap<LibraryName, Vec<CABILibrary>>,
196 ),
197 }
198
199 #[derive(Debug, Display, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
201 pub struct LibraryName(pub String);
202
203 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
204 pub enum LibraryType {
205 Static,
206 #[default]
207 Dynamic,
208 }
209
210 impl LibraryType {
211 pub(crate) fn cargo_lib_type(&self) -> &'static str {
212 match self {
213 Self::Static => "static",
214 Self::Dynamic => "dylib",
215 }
216 }
217
218 pub(crate) fn match_filename_suffix(suffix: &str) -> Option<Self> {
219 match suffix {
220 "a" => Some(Self::Static),
221 "so" => Some(Self::Dynamic),
222 _ => None,
223 }
224 }
225 }
226
227 #[derive(Debug, Clone)]
228 pub struct CABILibrary {
229 pub name: LibraryName,
230 pub path: PathBuf,
231 pub kind: LibraryType,
232 }
233
234 impl CABILibrary {
235 pub fn containing_directory(&self) -> &Path {
236 self
237 .path
238 .parent()
239 .expect("library path should have a parent directory!")
240 }
241
242 pub fn minus_l_arg(&self) -> String { format!("{}={}", self.kind.cargo_lib_type(), self.name) }
243
244 pub fn parse_file_path(file_path: &Path) -> Option<Self> {
245 static LIBNAME_PATTERN: Lazy<Regex> =
246 Lazy::new(|| Regex::new(r"^lib([^/]+)\.(a|so)$").unwrap());
247 let filename = match file_path.file_name() {
248 Some(component) => component.to_string_lossy(),
249 None => {
252 return None;
253 },
254 };
255 if let Some(m) = LIBNAME_PATTERN.captures(&filename) {
256 let name = LibraryName(m.get(1).unwrap().as_str().to_string());
257 let kind = LibraryType::match_filename_suffix(m.get(2).unwrap().as_str())
258 .expect("validated that only .a or .so files can match LIBNAME_PATTERN regex");
259 Some(Self {
260 path: file_path.to_path_buf(),
261 name,
262 kind,
263 })
264 } else {
265 None
266 }
267 }
268 }
269
270 #[derive(Debug, Clone)]
271 pub struct Prefix {
272 pub path: PathBuf,
273 }
274
275 impl Prefix {
276 pub fn traverse(&self) -> impl Stream<Item=Result<walkdir::DirEntry, walkdir::Error>> {
277 let path = self.path.clone();
278 try_stream! {
279 for file in walkdir::WalkDir::new(path) {
280 yield file?;
281 }
282 }
283 }
284
285 pub fn include_subdir(&self) -> PathBuf { self.path.join("include") }
286
287 pub fn lib_subdir(&self) -> PathBuf { self.path.join("lib") }
288 }
289
290 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
291 pub enum SearchBehavior {
292 #[default]
293 ErrorOnDuplicateLibraryName,
294 SelectFirstForEachLibraryName,
295 }
296
297 #[derive(Debug, Clone, Default)]
298 pub struct LibsQuery {
299 pub needed_libraries: Vec<LibraryName>,
300 pub kind: LibraryType,
301 pub search_behavior: SearchBehavior,
302 }
303
304 impl LibsQuery {
305 pub async fn find_libs(self, prefix: &Prefix) -> Result<LibCollection, PrefixTraversalError> {
306 let Self {
307 needed_libraries,
308 kind,
309 search_behavior,
310 } = self;
311 let needed_libraries: IndexSet<LibraryName> = needed_libraries.iter().cloned().collect();
312 let mut libs_by_name: IndexMap<LibraryName, Vec<CABILibrary>> = IndexMap::new();
313
314 let s = prefix.traverse();
315 pin_mut!(s);
316
317 while let Some(dir_entry) = s.try_next().await? {
318 let lib_path = dir_entry.into_path();
319 match CABILibrary::parse_file_path(&lib_path) {
320 Some(lib) if lib.kind == kind => {
321 dbg!(&lib);
322 libs_by_name
323 .entry(lib.name.clone())
324 .or_insert_with(Vec::new)
325 .push(lib);
326 },
327 _ => (),
328 }
329 }
330
331 let found: IndexSet<LibraryName> = libs_by_name.keys().cloned().collect();
332 let needed_not_found: IndexSet<LibraryName> =
333 needed_libraries.difference(&found).cloned().collect();
334 if !needed_not_found.is_empty() {
335 return Err(PrefixTraversalError::NeededLibrariesNotFound(
336 needed_not_found,
337 prefix.clone(),
338 found,
339 ));
340 }
341 let only_needed_libs: IndexMap<LibraryName, Vec<CABILibrary>> = libs_by_name
342 .into_iter()
343 .filter(|(name, _)| needed_libraries.contains(name))
344 .collect();
345
346 let mut singly_matched_libs: IndexMap<LibraryName, CABILibrary> = IndexMap::new();
347 let mut duplicated_libs: IndexMap<LibraryName, Vec<CABILibrary>> = IndexMap::new();
348 for (name, mut libs) in only_needed_libs.into_iter() {
349 assert!(!libs.is_empty());
350 if libs.len() == 1 {
351 singly_matched_libs.insert(name, libs.pop().unwrap());
352 } else {
353 duplicated_libs.insert(name, libs);
354 }
355 }
356
357 match search_behavior {
358 SearchBehavior::ErrorOnDuplicateLibraryName => {
359 if !duplicated_libs.is_empty() {
360 return Err(PrefixTraversalError::DuplicateLibraryNames(
361 duplicated_libs.keys().cloned().collect(),
362 prefix.clone(),
363 duplicated_libs,
364 ));
365 }
366 },
367 SearchBehavior::SelectFirstForEachLibraryName => {
368 for (name, libs) in duplicated_libs.into_iter() {
369 assert!(!singly_matched_libs.contains_key(&name));
370 assert!(!libs.is_empty());
371 dbg!(&name);
372 dbg!(&libs);
373 singly_matched_libs.insert(name, libs.into_iter().next().unwrap());
374 }
375 },
376 }
377 assert_eq!(singly_matched_libs.len(), needed_libraries.len());
378
379 Ok(LibCollection {
380 found_libraries: singly_matched_libs.into_values().collect(),
381 })
382 }
383 }
384
385 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
386 pub enum RpathBehavior {
387 #[default]
388 DoNotSetRpath,
389 SetRpathForContainingDirs,
390 }
391
392 #[derive(Debug, Clone)]
393 pub struct LibCollection {
394 pub found_libraries: Vec<CABILibrary>,
395 }
396
397 impl LibCollection {
398 pub fn link_libraries(self, rpath_behavior: RpathBehavior) {
399 let mut containing_dirs: IndexSet<PathBuf> = IndexSet::new();
400 for lib in self.found_libraries.into_iter() {
401 println!("cargo:rerun-if-changed={}", lib.path.display());
402 println!("cargo:rustc-link-lib={}", lib.minus_l_arg());
403 containing_dirs.insert(lib.containing_directory().to_path_buf());
404
405 if rpath_behavior == RpathBehavior::SetRpathForContainingDirs {
406 assert_eq!(lib.kind, LibraryType::Dynamic);
407 }
408 }
409
410 for dir in containing_dirs.iter() {
411 println!("cargo:rustc-link-search=native={}", dir.display());
412 }
413
414 if rpath_behavior == RpathBehavior::SetRpathForContainingDirs {
415 for dir in containing_dirs.iter() {
416 println!("cargo:rustc-link-arg=-Wl,-rpath,{}", dir.display());
417 }
418 let joined_rpath: String = containing_dirs
419 .into_iter()
420 .map(|p| format!("{}", p.display()))
421 .collect::<Vec<_>>()
422 .join(":");
423 println!("cargo:joined_rpath={}", joined_rpath);
424 }
425 }
426 }
427}
428
429pub mod metadata {
430 use crate::metadata_spec::spec;
431 use super_process::{exe, sync::SyncInvocable};
432
433 use displaydoc::Display;
434 use guppy::CargoMetadata;
435 use indexmap::{IndexMap, IndexSet};
436 use serde_json;
437 use thiserror::Error;
438
439 use std::{env, io};
440
441 #[derive(Debug, Display, Error)]
442 pub enum MetadataError {
443 Spec(#[from] spec::SpecError),
445 Command(#[from] exe::CommandErrorWrapper),
447 Io(#[from] io::Error),
449 Json(#[from] serde_json::Error),
451 Guppy(#[from] guppy::Error),
453 }
454
455 pub async fn get_metadata() -> Result<spec::DisjointResolves, MetadataError> {
456 let cargo_exe: exe::Exe = env::var("CARGO").as_ref().unwrap().into();
457 let cargo_argv: exe::Argv = ["metadata", "--format-version", "1"].into();
458 let cargo_cmd = exe::Command {
459 exe: cargo_exe,
460 argv: cargo_argv,
461 ..Default::default()
462 };
463
464 let metadata =
465 CargoMetadata::parse_json(cargo_cmd.clone().invoke().await?.decode(cargo_cmd)?.stdout)?;
466 let package_graph = metadata.build_graph()?;
467
468 let labelled_metadata: Vec<(
469 spec::CrateName,
470 spec::LabelledPackageMetadata,
471 Vec<spec::CargoFeature>,
472 )> = package_graph
473 .packages()
474 .filter_map(|p| {
475 let name = spec::CrateName(p.name().to_string());
476 let spack_metadata = p.metadata_table().as_object()?.get("spack")?.clone();
477 dbg!(&spack_metadata);
478 let features: Vec<_> = p
479 .named_features()
480 .map(|s| spec::CargoFeature(s.to_string()))
481 .collect();
482 Some((name, spack_metadata, features))
483 })
484 .map(|(name, spack_metadata, features)| {
485 let spec_metadata: spec::LabelledPackageMetadata = serde_json::from_value(spack_metadata)?;
486 Ok((name, spec_metadata, features))
487 })
488 .collect::<Result<Vec<_>, MetadataError>>()?;
489
490 let mut resolves: IndexMap<spec::Label, (Option<spec::Repo>, Vec<spec::Spec>)> =
491 IndexMap::new();
492 let mut recipes: IndexMap<
493 spec::CrateName,
494 IndexMap<spec::Label, (spec::Recipe, spec::FeatureMap)>,
495 > = IndexMap::new();
496 let mut declared_features_by_package: IndexMap<spec::CrateName, Vec<spec::CargoFeature>> =
497 IndexMap::new();
498
499 for (crate_name, spec::LabelledPackageMetadata { envs, repo }, features) in
500 labelled_metadata.into_iter()
501 {
502 dbg!(&envs);
503
504 assert!(declared_features_by_package
505 .insert(crate_name.clone(), features)
506 .is_none());
507
508 for (label, env) in envs.into_iter() {
509 let spec::Env {
510 spec,
511 deps,
512 features,
513 } = env;
514 let env_label = spec::Label(label);
515 let spec = spec::Spec(spec);
516 let feature_map = if let Some(spec::FeatureLayout {
517 needed,
518 conflicting,
519 }) = features
520 {
521 let needed: IndexSet<_> = needed
522 .unwrap_or_default()
523 .into_iter()
524 .map(spec::CargoFeature)
525 .collect();
526 let conflicting: IndexSet<_> = conflicting
527 .unwrap_or_default()
528 .into_iter()
529 .map(spec::CargoFeature)
530 .collect();
531 spec::FeatureMap {
532 needed,
533 conflicting,
534 }
535 } else {
536 spec::FeatureMap::default()
537 };
538
539 let sub_deps: Vec<spec::SubDep> = deps
540 .into_iter()
541 .map(|(pkg_name, spec::Dep { r#type, lib_names })| {
542 Ok(spec::SubDep {
543 pkg_name: spec::PackageName(pkg_name),
544 r#type: r#type.parse()?,
545 lib_names,
546 })
547 })
548 .collect::<Result<Vec<_>, spec::SpecError>>()?;
549
550 let recipe = spec::Recipe { sub_deps };
551
552 let r = resolves.entry(env_label.clone()).or_default();
553 r.0 = repo.clone();
554 r.1.push(spec);
555 assert!(recipes
556 .entry(crate_name.clone())
557 .or_default()
558 .insert(env_label, (recipe, feature_map))
559 .is_none());
560 }
561 }
562
563 Ok(spec::DisjointResolves {
564 by_label: resolves
565 .into_iter()
566 .map(|(label, (repo, specs))| (label, spec::EnvInstructions { repo, specs }))
567 .collect(),
568 recipes,
569 declared_features_by_package,
570 })
571 }
572
573 pub fn get_cur_pkg_name() -> spec::CrateName {
574 spec::CrateName(env::var("CARGO_PKG_NAME").unwrap())
575 }
576}
577
578pub mod declarative {
580 pub mod resolve {
581 use crate::{
582 commands::*,
583 metadata_spec::spec,
584 subprocess::spack::SpackInvocation,
585 utils::{declarative::linker, metadata, prefix},
586 };
587
588 use std::borrow::Cow;
589
590 use indexmap::IndexMap;
591
592 fn currently_has_feature(feature: &spec::CargoFeature) -> bool {
593 std::env::var(feature.to_env_var_name()).is_ok()
594 }
595
596 async fn resolve_recipe(
597 env_label: spec::Label,
598 recipe: &spec::Recipe,
599 env_instructions: &spec::EnvInstructions,
600 ) -> eyre::Result<Vec<prefix::Prefix>> {
601 let env_hash = env_instructions.compute_digest();
602 let env = EnvName(env_hash.hashed_env_name(&env_label.0));
603 dbg!(&env);
604
605 let spack = SpackInvocation::summon().await?;
607
608 let env = env::EnvCreate {
611 spack: spack.clone(),
612 env,
613 }
614 .idempotent_env_create(Cow::Borrowed(env_instructions))
615 .await?;
616
617 let mut dep_prefixes: Vec<prefix::Prefix> = Vec::new();
618
619 for sub_dep in recipe.sub_deps.iter() {
621 let env = env.clone();
622 let spack = spack.clone();
623 let req = find::FindPrefix {
624 spack: spack.clone(),
625 spec: CLISpec::new(&sub_dep.pkg_name.0),
626 env: Some(env.clone()),
627 repos: match env_instructions.repo.clone() {
628 Some(spec::Repo { path }) => {
629 let path = std::env::current_dir()?.join(path);
630 println!("cargo:rerun-if-changed={}", path.display());
631 Some(RepoDirs(vec![path]))
632 },
633 None => None,
634 },
635 };
636 let prefix = req.find_prefix().await?.unwrap();
637
638 linker::link_against_dependency(
639 prefix.clone(),
640 sub_dep.r#type,
641 sub_dep.lib_names.iter().map(|s| s.as_str()),
642 )
643 .await?;
644 dep_prefixes.push(prefix);
645 }
646
647 Ok(dep_prefixes)
648 }
649
650 pub async fn resolve_dependencies_for_label(
651 env_label: spec::Label,
652 ) -> eyre::Result<Vec<prefix::Prefix>> {
653 println!("cargo:rerun-if-changed=Cargo.toml");
654
655 let metadata = metadata::get_metadata().await?;
657 let cur_pkg_name = metadata::get_cur_pkg_name();
659
660 let declared_recipes: &IndexMap<spec::Label, (spec::Recipe, spec::FeatureMap)> =
662 metadata.recipes.get(&cur_pkg_name).unwrap();
663 dbg!(declared_recipes);
664 let specified_recipe: &spec::Recipe = declared_recipes
665 .get(&env_label)
666 .map(|(recipe, _)| recipe)
667 .ok_or_else(|| {
668 eyre::Report::msg(format!(
669 "unable to find label {:?} in declarations {:?}",
670 &env_label, declared_recipes,
671 ))
672 })?;
673
674 let env_instructions = metadata.by_label.get(&env_label).unwrap();
675 dbg!(env_instructions);
676
677 resolve_recipe(env_label, specified_recipe, env_instructions).await
678 }
679
680 pub async fn resolve_dependencies() -> eyre::Result<Vec<prefix::Prefix>> {
681 println!("cargo:rerun-if-changed=Cargo.toml");
682
683 let metadata = metadata::get_metadata().await?;
685 let cur_pkg_name = metadata::get_cur_pkg_name();
687
688 let declared_recipes: &IndexMap<spec::Label, (spec::Recipe, spec::FeatureMap)> =
690 metadata.recipes.get(&cur_pkg_name).unwrap();
691 dbg!(&declared_recipes);
692
693 let declared_features = metadata
695 .declared_features_by_package
696 .get(&cur_pkg_name)
697 .unwrap();
698 let activated_features = spec::FeatureSet(
699 declared_features
700 .iter()
701 .filter(|f| currently_has_feature(f))
702 .cloned()
703 .collect(),
704 );
705
706 let matching_recipes: Vec<(&spec::Label, &spec::Recipe)> = declared_recipes
708 .into_iter()
709 .filter(|(_, (_, feature_map))| feature_map.evaluate(&activated_features))
710 .map(|(label, (recipe, _))| (label, recipe))
711 .collect();
712
713 let (env_label, cur_recipe) = match matching_recipes.len() {
714 0 => {
715 return Err(eyre::Report::msg(format!(
716 "no recipe found for given features {:?} from declared recipes {:?}",
717 activated_features, declared_recipes,
718 )))
719 },
720 1 => matching_recipes[0],
721 _ => {
722 return Err(eyre::Report::msg(format!(
723 "more than one recipe {:?} matched for given features {:?} from declared recipes {:?}",
724 matching_recipes, activated_features, declared_recipes,
725 )))
726 },
727 };
728 dbg!(&env_label);
729 dbg!(&cur_recipe);
730
731 let env_instructions = metadata.by_label.get(env_label).unwrap();
733 dbg!(env_instructions);
734
735 resolve_recipe(env_label.clone(), cur_recipe, env_instructions).await
736 }
737 }
738
739 pub mod linker {
740 use crate::{metadata_spec::spec, utils::prefix};
741
742 pub async fn link_against_dependency(
743 prefix: prefix::Prefix,
744 r#type: spec::LibraryType,
745 lib_names: impl Iterator<Item=&str>,
746 ) -> eyre::Result<()> {
747 let needed_libraries: Vec<_> = lib_names
748 .map(|s| prefix::LibraryName(s.to_string()))
749 .collect();
750 let kind = match r#type {
751 spec::LibraryType::Static => prefix::LibraryType::Static,
752 spec::LibraryType::DynamicWithRpath => prefix::LibraryType::Dynamic,
753 };
754 let query = prefix::LibsQuery {
755 needed_libraries,
756 kind,
757 search_behavior: prefix::SearchBehavior::ErrorOnDuplicateLibraryName,
758 };
759 let libs = query.find_libs(&prefix).await?;
760
761 let rpath_behavior = match kind {
762 prefix::LibraryType::Static => prefix::RpathBehavior::DoNotSetRpath,
764 prefix::LibraryType::Dynamic => prefix::RpathBehavior::SetRpathForContainingDirs,
765 };
766 libs.link_libraries(rpath_behavior);
767
768 Ok(())
769 }
770 }
771}