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