1#![forbid(unsafe_code)]
4
5use {
6 anyhow::{Context as _, Result, bail, ensure},
7 fs2::FileExt as _,
8 serde::{Deserialize, Serialize},
9 sha2::{Digest as _, Sha256},
10 std::{
11 env,
12 ffi::OsStr,
13 fs::{self, File, OpenOptions},
14 io::{self, Read as _, Write as _},
15 path::{Component, Path, PathBuf},
16 str::FromStr,
17 },
18 zip::ZipArchive,
19};
20
21const LOCK_MANIFEST: &str = include_str!("../sdk-lock.toml");
22
23fn cache_dir() -> Result<PathBuf> {
24 if let Some(path) = env::var_os("NVIDIA_SDK_CACHE").filter(|value| !value.is_empty()) {
25 return Ok(PathBuf::from(path));
26 }
27
28 if cfg!(windows) {
29 return env::var_os("LOCALAPPDATA")
30 .map(PathBuf::from)
31 .map(|path| path.join("nvidia-sdk"))
32 .context("set NVIDIA_SDK_CACHE or LOCALAPPDATA");
33 }
34
35 if let Some(path) = env::var_os("XDG_CACHE_HOME").filter(|value| !value.is_empty()) {
36 return Ok(PathBuf::from(path).join("nvidia-sdk"));
37 }
38
39 env::var_os("HOME")
40 .map(PathBuf::from)
41 .map(|path| path.join(".cache/nvidia-sdk"))
42 .context("set NVIDIA_SDK_CACHE or HOME")
43}
44
45fn download(url: &str, destination: &Path) -> Result<()> {
46 let response = ureq::get(url)
47 .set(
48 "User-Agent",
49 concat!("nvidia-sdk/", env!("CARGO_PKG_VERSION")),
50 )
51 .call()
52 .with_context(|| format!("downloading {url}"))?;
53 let mut source = response.into_reader();
54 let mut destination_file = File::create(destination)
55 .with_context(|| format!("creating download file {}", destination.display()))?;
56 io::copy(&mut source, &mut destination_file)
57 .with_context(|| format!("downloading {url} to {}", destination.display()))?;
58 destination_file
59 .flush()
60 .with_context(|| format!("flushing download file {}", destination.display()))?;
61
62 Ok(())
63}
64
65fn extract_zip(archive_path: &Path, output: &Path) -> Result<()> {
66 fs::create_dir_all(output)
67 .with_context(|| format!("creating extraction directory {}", output.display()))?;
68 let file = File::open(archive_path)
69 .with_context(|| format!("opening archive {}", archive_path.display()))?;
70 let mut archive = ZipArchive::new(file)
71 .with_context(|| format!("reading zip archive {}", archive_path.display()))?;
72
73 for index in 0..archive.len() {
74 let mut entry = archive.by_index(index).with_context(|| {
75 format!(
76 "reading entry {index} in archive {}",
77 archive_path.display()
78 )
79 })?;
80 let relative = entry.enclosed_name().with_context(|| {
81 format!(
82 "unsafe path in entry {index} of archive {}",
83 archive_path.display()
84 )
85 })?;
86 safe_relative_path(&relative).with_context(|| {
87 format!(
88 "validating entry {index} in archive {}",
89 archive_path.display()
90 )
91 })?;
92 let destination = output.join(relative);
93
94 if entry.is_dir() {
95 fs::create_dir_all(&destination)
96 .with_context(|| format!("creating archive directory {}", destination.display()))?;
97
98 continue;
99 }
100
101 if let Some(parent) = destination.parent() {
102 fs::create_dir_all(parent)
103 .with_context(|| format!("creating extraction parent {}", parent.display()))?;
104 }
105
106 let mut file = File::create(&destination)
107 .with_context(|| format!("creating extracted file {}", destination.display()))?;
108 io::copy(&mut entry, &mut file).with_context(|| {
109 format!(
110 "extracting entry {index} from {} to {}",
111 archive_path.display(),
112 destination.display()
113 )
114 })?;
115 }
116
117 Ok(())
118}
119
120fn verify_hash(path: &Path, expected: &str) -> Result<()> {
121 let mut file = File::open(path)
122 .with_context(|| format!("opening archive for hashing {}", path.display()))?;
123 let mut hasher = Sha256::new();
124 io::copy(&mut file, &mut hasher)
125 .with_context(|| format!("hashing archive {}", path.display()))?;
126 let actual = format!("{:x}", hasher.finalize());
127
128 ensure!(
129 actual == expected,
130 "archive hash mismatch for {}: expected {expected}, got {actual}",
131 path.display()
132 );
133
134 Ok(())
135}
136
137fn remove_path(path: &Path) -> Result<()> {
138 match fs::symlink_metadata(path) {
139 Ok(metadata) if metadata.file_type().is_symlink() || metadata.is_file() => {
140 fs::remove_file(path).with_context(|| format!("removing file {}", path.display()))?;
141 }
142 Ok(metadata) if metadata.is_dir() => fs::remove_dir_all(path)
143 .with_context(|| format!("removing directory {}", path.display()))?,
144 Ok(_) => bail!("unsupported filesystem object at {}", path.display()),
145 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
146 Err(error) => {
147 return Err(error).with_context(|| format!("reading metadata for {}", path.display()));
148 }
149 }
150
151 Ok(())
152}
153
154fn safe_relative_path(path: &Path) -> Result<()> {
155 ensure!(!path.as_os_str().is_empty(), "empty relative path");
156
157 ensure!(
158 !path.is_absolute(),
159 "absolute path is not allowed: {}",
160 path.display()
161 );
162
163 ensure!(
164 path.components()
165 .all(|component| matches!(component, Component::Normal(_))),
166 "unsafe relative path: {}",
167 path.display()
168 );
169
170 Ok(())
171}
172
173#[derive(Debug, Deserialize, Serialize)]
174struct Artifact {
175 assembly: Option<SourceAssembly>,
176 id: String,
177 required: Vec<PathBuf>,
178 runtime: Option<Vec<RuntimeFile>>,
179 sha256: String,
180 targets: Vec<String>,
181 url: String,
182}
183
184impl Artifact {
185 fn acquire(&self, path: &Path, sdk: Sdk, entry: &SdkEntry) -> Result<()> {
186 ensure!(
187 self.url.starts_with("https://")
188 && self.sha256.len() == 64
189 && self.sha256.bytes().all(|byte| byte.is_ascii_hexdigit()),
190 "invalid acquisition metadata for {} {}",
191 sdk.name(),
192 self.id
193 );
194
195 let parent = path
196 .parent()
197 .with_context(|| format!("sdk cache path has no parent: {}", path.display()))?;
198 fs::create_dir_all(parent)
199 .with_context(|| format!("creating cache directory {}", parent.display()))?;
200 let key = self.cache_key()?;
201 let lock_path = parent.join(format!("{key}.lock"));
202 let lock = OpenOptions::new()
203 .create(true)
204 .read(true)
205 .truncate(false)
206 .write(true)
207 .open(&lock_path)
208 .with_context(|| format!("opening cache lock {}", lock_path.display()))?;
209 lock.lock_exclusive()
210 .with_context(|| format!("locking cache file {}", lock_path.display()))?;
211
212 if self.validate_cached(path).is_ok() {
213 return Ok(());
214 }
215
216 eprintln!(
217 "acquiring nvidia {} sdk {} from {}\nlicense: {} ({})",
218 sdk.name(),
219 entry.version,
220 self.url,
221 entry.license,
222 entry.license_url
223 );
224
225 let temporary = parent.join(format!(".{key}.{}.tmp", std::process::id()));
226 remove_path(&temporary)?;
227 fs::create_dir_all(&temporary)
228 .with_context(|| format!("creating staging directory {}", temporary.display()))?;
229 let result = (|| -> Result<()> {
230 let archive_path = temporary.join("archive.zip");
231 download(&self.url, &archive_path)?;
232 verify_hash(&archive_path, &self.sha256)?;
233 let extracted = temporary.join("extracted");
234 extract_zip(&archive_path, &extracted)?;
235 let root = if let Some(assembly) = &self.assembly {
236 safe_relative_path(&assembly.root)?;
237 let root = extracted.join(&assembly.root);
238
239 ensure!(
240 root.is_dir(),
241 "missing source archive root {}",
242 root.display()
243 );
244
245 assembly.prepare(&root, &temporary)?;
246
247 root
248 } else {
249 self.locate_root(&extracted)?
250 };
251 self.validate(&root)?;
252
253 if self.assembly.is_some() {
254 let stamp = root.join(".nvidia-sdk-source");
255 fs::write(&stamp, &key).with_context(|| {
256 format!("writing source assembly stamp {}", stamp.display())
257 })?;
258 }
259
260 remove_path(path)?;
261 fs::rename(&root, path).with_context(|| {
262 format!(
263 "installing downloaded sdk from {} to {}",
264 root.display(),
265 path.display()
266 )
267 })?;
268
269 Ok(())
270 })();
271 let cleanup = remove_path(&temporary);
272 result?;
273
274 cleanup
275 }
276
277 fn cache_key(&self) -> Result<String> {
278 if self.assembly.is_some() {
280 Ok(format!(
281 "{:x}",
282 Sha256::digest(serde_json::to_vec(self).with_context(|| format!(
283 "serializing cache identity for artifact {}",
284 self.id
285 ))?)
286 ))
287 } else {
288 Ok(self.sha256.clone())
289 }
290 }
291
292 fn validate_cached(&self, path: &Path) -> Result<()> {
293 self.validate(path)?;
294
295 if self.assembly.is_some() {
296 let stamp = path.join(".nvidia-sdk-source");
297
298 ensure!(
299 fs::read_to_string(&stamp).with_context(|| format!(
300 "reading source assembly stamp {}",
301 stamp.display()
302 ))? == self.cache_key()?,
303 "source sdk assembly is incomplete or stale: {}",
304 stamp.display()
305 );
306 }
307
308 Ok(())
309 }
310
311 fn locate_root(&self, extracted: &Path) -> Result<PathBuf> {
312 if self.validate(extracted).is_ok() {
313 return Ok(extracted.to_owned());
314 }
315
316 let mut candidates = fs::read_dir(extracted)
317 .with_context(|| format!("reading extracted directory {}", extracted.display()))?
318 .filter_map(Result::ok)
319 .filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_dir()))
320 .map(|entry| entry.path())
321 .filter(|path| self.validate(path).is_ok());
322 let root = candidates.next().with_context(|| {
323 format!(
324 "archive does not contain the expected sdk layout: {}",
325 extracted.display()
326 )
327 })?;
328
329 ensure!(
330 candidates.next().is_none(),
331 "archive contains multiple possible sdk roots: {}",
332 extracted.display()
333 );
334
335 Ok(root)
336 }
337
338 fn supports(&self, target: &str) -> bool {
339 self.targets
340 .iter()
341 .any(|candidate| candidate == "*" || candidate == target)
342 }
343
344 fn validate(&self, path: &Path) -> Result<()> {
345 ensure!(
346 path.is_dir(),
347 "sdk directory is missing: {}",
348 path.display()
349 );
350
351 for relative in &self.required {
352 safe_relative_path(relative)?;
353 let required = path.join(relative);
354 let metadata = fs::metadata(&required).with_context(|| {
355 format!(
356 "reading metadata for required sdk file {}",
357 required.display()
358 )
359 })?;
360
361 ensure!(
362 metadata.is_file(),
363 "sdk input is not a file: {}",
364 required.display()
365 );
366
367 ensure!(
368 metadata.len() > 0,
369 "sdk input is empty: {}",
370 required.display()
371 );
372
373 let mut prefix = [0_u8; 128];
374 let count = File::open(&required)
375 .with_context(|| format!("opening required sdk file {}", required.display()))?
376 .read(&mut prefix)
377 .with_context(|| format!("reading required sdk file {}", required.display()))?;
378
379 ensure!(
380 !prefix[..count].starts_with(b"version https://git-lfs.github.com/spec/v1"),
381 "sdk input is a git lfs pointer: {}",
382 required.display()
383 );
384 }
385
386 Ok(())
387 }
388}
389
390#[derive(Debug, Deserialize)]
391struct LockManifest {
392 schema: u32,
393 sdks: SdkEntries,
394}
395
396impl LockManifest {
397 fn load() -> Result<Self> {
398 let manifest: Self = toml::from_str(LOCK_MANIFEST).context("parsing sdk-lock.toml")?;
399
400 ensure!(manifest.schema == 1, "unsupported sdk lock schema");
401
402 Ok(manifest)
403 }
404}
405
406#[derive(Clone, Debug, Serialize)]
408pub struct ResolvedSdk {
409 pub artifact: String,
410 pub license: String,
411 pub license_url: String,
412 pub path: PathBuf,
413 pub sdk: Sdk,
414 pub source_url: Option<String>,
415 pub target: String,
416 pub version: String,
417}
418
419impl ResolvedSdk {
420 fn new(
421 sdk: Sdk,
422 target: &str,
423 entry: &SdkEntry,
424 artifact: &Artifact,
425 path: PathBuf,
426 source_url: Option<String>,
427 ) -> Self {
428 Self {
429 artifact: artifact.id.clone(),
430 license: entry.license.clone(),
431 license_url: entry.license_url.clone(),
432 path,
433 sdk,
434 source_url,
435 target: target.to_owned(),
436 version: entry.version.clone(),
437 }
438 }
439
440 pub fn stage_runtime(&self, output: &Path, profile: RuntimeProfile) -> Result<Vec<StagedFile>> {
447 let manifest = LockManifest::load()?;
448 let entry = manifest.sdks.get(self.sdk);
449 let artifact = entry
450 .artifacts
451 .iter()
452 .find(|artifact| artifact.id == self.artifact)
453 .context("resolved artifact is absent from the embedded lock manifest")?;
454 let runtime = artifact
455 .runtime
456 .as_deref()
457 .context("sdk artifact does not define a redistributable runtime")?;
458 fs::create_dir_all(output)
459 .with_context(|| format!("creating runtime directory {}", output.display()))?;
460
461 let mut staged = Vec::new();
462
463 for file in runtime.iter().filter(|file| {
464 file.profile
465 .as_deref()
466 .is_none_or(|candidate| candidate == profile.name())
467 }) {
468 safe_relative_path(&file.source)?;
469 safe_relative_path(&file.destination)?;
470 let source = self.path.join(&file.source);
471 let destination = output.join(&file.destination);
472
473 ensure!(
474 source.is_file(),
475 "missing runtime file {}",
476 source.display()
477 );
478
479 if let Some(parent) = destination.parent() {
480 fs::create_dir_all(parent)
481 .with_context(|| format!("creating runtime parent {}", parent.display()))?;
482 }
483
484 fs::copy(&source, &destination).with_context(|| {
485 format!(
486 "copying runtime file {} to {}",
487 source.display(),
488 destination.display()
489 )
490 })?;
491 staged.push(StagedFile {
492 path: destination,
493 source,
494 });
495 }
496
497 Ok(staged)
498 }
499}
500
501#[derive(Clone, Debug)]
503pub struct ResolveOptions {
504 pub cache_dir: Option<PathBuf>,
505 pub offline: bool,
506 pub target: String,
507}
508
509impl ResolveOptions {
510 fn environment_flag(name: &str) -> bool {
511 env::var_os(name).is_some_and(|value| {
512 !value.is_empty() && value != OsStr::new("0") && value != OsStr::new("false")
513 })
514 }
515
516 pub fn for_cargo() -> Result<Self> {
522 let target = env::var("TARGET").context("cargo did not provide TARGET")?;
523
524 Ok(Self {
525 cache_dir: None,
526 offline: Self::environment_flag("NVIDIA_SDK_OFFLINE")
527 || Self::environment_flag("CARGO_NET_OFFLINE"),
528 target,
529 })
530 }
531
532 #[must_use]
533 pub fn for_target(target: impl Into<String>) -> Self {
534 Self {
535 cache_dir: None,
536 offline: Self::environment_flag("NVIDIA_SDK_OFFLINE")
537 || Self::environment_flag("CARGO_NET_OFFLINE"),
538 target: target.into(),
539 }
540 }
541}
542
543#[derive(Debug, Deserialize, Serialize)]
544struct RuntimeFile {
545 destination: PathBuf,
546 profile: Option<String>,
547 source: PathBuf,
548}
549
550#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
552pub enum RuntimeProfile {
553 #[default]
554 Release,
555 Development,
556}
557
558impl RuntimeProfile {
559 const fn name(self) -> &'static str {
560 match self {
561 Self::Release => "release",
562 Self::Development => "development",
563 }
564 }
565}
566
567impl FromStr for RuntimeProfile {
568 type Err = anyhow::Error;
569
570 fn from_str(value: &str) -> Result<Self> {
571 match value {
572 "release" => Ok(Self::Release),
573 "development" => Ok(Self::Development),
574 _ => bail!("unknown runtime profile {value:?}"),
575 }
576 }
577}
578
579#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
581#[serde(rename_all = "kebab-case")]
582pub enum Sdk {
583 Dlss,
584 Nrd,
585 Omm,
586 Sharc,
587 Streamline,
588}
589
590impl Sdk {
591 #[must_use]
592 pub const fn name(self) -> &'static str {
593 match self {
594 Self::Dlss => "dlss",
595 Self::Nrd => "nrd",
596 Self::Omm => "omm",
597 Self::Sharc => "sharc",
598 Self::Streamline => "streamline",
599 }
600 }
601
602 pub fn resolve(self, options: &ResolveOptions) -> Result<ResolvedSdk> {
609 let manifest = LockManifest::load()?;
610 let entry = manifest.sdks.get(self);
611 let artifact = entry
612 .artifacts
613 .iter()
614 .find(|artifact| artifact.supports(&options.target))
615 .with_context(|| {
616 format!(
617 "{} {} has no pinned artifact for {}",
618 self.name(),
619 entry.version,
620 options.target
621 )
622 })?;
623
624 if let Some(path) = env::var_os(&entry.environment).filter(|value| !value.is_empty()) {
625 let path = PathBuf::from(path);
626 artifact.validate(&path).with_context(|| {
627 format!(
628 "{} points to an invalid {} {} sdk",
629 entry.environment,
630 self.name(),
631 entry.version
632 )
633 })?;
634
635 return Ok(ResolvedSdk::new(
636 self,
637 &options.target,
638 entry,
639 artifact,
640 path,
641 None,
642 ));
643 }
644
645 if let Some(root) = env::var_os("NVIDIA_SDK_ROOT").filter(|value| !value.is_empty()) {
646 let path = PathBuf::from(root).join(self.name()).join(&entry.version);
647
648 if path.exists() {
649 artifact.validate(&path)?;
650
651 return Ok(ResolvedSdk::new(
652 self,
653 &options.target,
654 entry,
655 artifact,
656 path,
657 None,
658 ));
659 }
660 }
661
662 let default_cache;
663 let cache = if let Some(cache) = &options.cache_dir {
664 cache
665 } else {
666 default_cache = cache_dir()?;
667 &default_cache
668 };
669 let path = cache
670 .join(self.name())
671 .join(&entry.version)
672 .join(artifact.cache_key()?);
673
674 if artifact.validate_cached(&path).is_ok() {
675 return Ok(ResolvedSdk::new(
676 self,
677 &options.target,
678 entry,
679 artifact,
680 path,
681 Some(artifact.url.clone()),
682 ));
683 }
684
685 ensure!(
686 !options.offline,
687 "{} {} is not cached and downloads are disabled",
688 self.name(),
689 entry.version
690 );
691
692 artifact.acquire(&path, self, entry)?;
693
694 Ok(ResolvedSdk::new(
695 self,
696 &options.target,
697 entry,
698 artifact,
699 path,
700 Some(artifact.url.clone()),
701 ))
702 }
703}
704
705impl FromStr for Sdk {
706 type Err = anyhow::Error;
707
708 fn from_str(value: &str) -> Result<Self> {
709 match value {
710 "dlss" => Ok(Self::Dlss),
711 "nrd" => Ok(Self::Nrd),
712 "omm" => Ok(Self::Omm),
713 "sharc" => Ok(Self::Sharc),
714 "streamline" => Ok(Self::Streamline),
715 _ => bail!("unknown nvidia sdk {value:?}"),
716 }
717 }
718}
719
720#[derive(Debug, Deserialize)]
721struct SdkEntries {
722 dlss: SdkEntry,
723 nrd: SdkEntry,
724 omm: SdkEntry,
725 sharc: SdkEntry,
726 streamline: SdkEntry,
727}
728
729impl SdkEntries {
730 fn get(&self, sdk: Sdk) -> &SdkEntry {
731 match sdk {
732 Sdk::Dlss => &self.dlss,
733 Sdk::Nrd => &self.nrd,
734 Sdk::Omm => &self.omm,
735 Sdk::Sharc => &self.sharc,
736 Sdk::Streamline => &self.streamline,
737 }
738 }
739}
740
741#[derive(Debug, Deserialize)]
742struct SdkEntry {
743 artifacts: Vec<Artifact>,
744 environment: String,
745 license: String,
746 license_url: String,
747 version: String,
748}
749
750#[derive(Debug, Deserialize, Serialize)]
751struct SourceAssembly {
752 dependencies: Vec<SourceDependency>,
753 #[serde(default)]
754 patches: Vec<SourcePatch>,
755 #[serde(default)]
756 relocations: Vec<SourceRelocation>,
757 root: PathBuf,
758}
759
760impl SourceAssembly {
761 fn prepare(&self, root: &Path, temporary: &Path) -> Result<()> {
762 for relocation in &self.relocations {
763 relocation.apply(root, root)?;
764 }
765
766 for (index, dependency) in self.dependencies.iter().enumerate() {
767 ensure!(
768 dependency.url.starts_with("https://")
769 && dependency.sha256.len() == 64
770 && dependency
771 .sha256
772 .bytes()
773 .all(|byte| byte.is_ascii_hexdigit()),
774 "invalid source dependency acquisition metadata"
775 );
776
777 let archive = temporary.join(format!("dependency-{index}.zip"));
778 download(&dependency.url, &archive)?;
779 verify_hash(&archive, &dependency.sha256)?;
780 let extracted = temporary.join(format!("dependency-{index}"));
781 extract_zip(&archive, &extracted)?;
782
783 if let Some(files) = &dependency.files {
784 safe_relative_path(&dependency.root)?;
785 safe_relative_path(&dependency.destination)?;
786 let source = extracted.join(&dependency.root);
787 let destination = root.join(&dependency.destination);
788
789 ensure!(
790 !destination.exists(),
791 "source dependency destination already exists: {}",
792 destination.display()
793 );
794
795 ensure!(!files.is_empty(), "empty source dependency file selection");
796
797 for relative in files {
798 safe_relative_path(relative)?;
799 let output = destination.join(relative);
800 let parent = output.parent().with_context(|| {
801 format!("dependency path has no parent: {}", output.display())
802 })?;
803 fs::create_dir_all(parent).with_context(|| {
804 format!("creating dependency directory {}", parent.display())
805 })?;
806 let input = source.join(relative);
807 fs::copy(&input, &output).with_context(|| {
808 format!(
809 "copying source dependency {} to {}",
810 input.display(),
811 output.display()
812 )
813 })?;
814 }
815 } else {
816 SourceRelocation {
817 source: dependency.root.clone(),
818 destination: dependency.destination.clone(),
819 }
820 .apply(&extracted, root)?;
821 }
822
823 remove_path(&extracted)?;
824 fs::remove_file(&archive)
825 .with_context(|| format!("removing dependency archive {}", archive.display()))?;
826 }
827
828 for patch in &self.patches {
829 patch.apply(root)?;
830 }
831
832 Ok(())
833 }
834}
835
836#[derive(Debug, Deserialize, Serialize)]
837struct SourceDependency {
838 destination: PathBuf,
839 #[serde(skip_serializing_if = "Option::is_none")]
840 files: Option<Vec<PathBuf>>,
841 root: PathBuf,
842 sha256: String,
843 url: String,
844}
845
846#[derive(Debug, Deserialize, Serialize)]
847struct SourcePatch {
848 after: String,
849 before: String,
850 path: PathBuf,
851}
852
853impl SourcePatch {
854 fn apply(&self, root: &Path) -> Result<()> {
855 safe_relative_path(&self.path)?;
856 let path = root.join(&self.path);
857 let source = fs::read_to_string(&path)
858 .with_context(|| format!("reading source patch input {}", path.display()))?;
859
860 ensure!(
861 !self.before.is_empty() && source.matches(&self.before).count() == 1,
862 "source patch preimage does not match exactly once: {}",
863 path.display()
864 );
865
866 fs::write(&path, source.replacen(&self.before, &self.after, 1))
867 .with_context(|| format!("writing patched source {}", path.display()))?;
868
869 Ok(())
870 }
871}
872
873#[derive(Debug, Deserialize, Serialize)]
874struct SourceRelocation {
875 destination: PathBuf,
876 source: PathBuf,
877}
878
879impl SourceRelocation {
880 fn apply(&self, source_root: &Path, destination_root: &Path) -> Result<()> {
881 safe_relative_path(&self.source)?;
882 safe_relative_path(&self.destination)?;
883 let source = source_root.join(&self.source);
884 let destination = destination_root.join(&self.destination);
885
886 ensure!(
887 !destination.exists(),
888 "source assembly destination already exists: {}",
889 destination.display()
890 );
891
892 let parent = destination
893 .parent()
894 .with_context(|| format!("assembly path has no parent: {}", destination.display()))?;
895 fs::create_dir_all(parent)
896 .with_context(|| format!("creating assembly directory {}", parent.display()))?;
897 fs::rename(&source, &destination).with_context(|| {
898 format!(
899 "assembling {} into {}",
900 source.display(),
901 destination.display()
902 )
903 })?;
904
905 Ok(())
906 }
907}
908
909#[derive(Clone, Debug, Serialize)]
911pub struct StagedFile {
912 pub path: PathBuf,
913 pub source: PathBuf,
914}
915
916#[cfg(test)]
917mod tests {
918 use super::*;
919
920 #[test]
921 fn parses_lock_manifest() {
922 let manifest = LockManifest::load().unwrap();
923
924 assert_eq!(manifest.sdks.dlss.version, "310.4.0");
925 assert_eq!(manifest.sdks.streamline.artifacts.len(), 1);
926 }
927
928 #[test]
929 fn rejects_unsafe_paths() {
930 assert!(safe_relative_path(Path::new("include/header.h")).is_ok());
931 assert!(safe_relative_path(Path::new("../header.h")).is_err());
932 assert!(safe_relative_path(Path::new("/tmp/header.h")).is_err());
933 }
934
935 #[test]
936 fn shader_artifact_supports_all_targets_but_native_artifacts_remain_exact() {
937 let manifest = LockManifest::load().unwrap();
938
939 assert_eq!("sharc".parse::<Sdk>().unwrap(), Sdk::Sharc);
940 assert_eq!(Sdk::Sharc.name(), "sharc");
941 assert_eq!(serde_json::to_string(&Sdk::Sharc).unwrap(), "\"sharc\"");
942 assert_eq!(manifest.sdks.sharc.version, "1.8.3");
943 assert_eq!(manifest.sdks.sharc.environment, "SHARC_SDK");
944
945 let artifact = &manifest.sdks.sharc.artifacts[0];
946
947 assert!(artifact.assembly.is_none());
948 assert!(artifact.runtime.is_none());
949
950 for target in [
951 "x86_64-unknown-linux-gnu",
952 "x86_64-pc-windows-msvc",
953 "aarch64-apple-darwin",
954 "x86_64-apple-darwin",
955 "wasm32-unknown-unknown",
956 "future-target",
957 ] {
958 assert!(artifact.supports(target));
959
960 for sdk in [Sdk::Dlss, Sdk::Nrd, Sdk::Omm, Sdk::Streamline] {
961 for native in &manifest.sdks.get(sdk).artifacts {
962 assert!(!native.targets.iter().any(|target| target.contains('*')));
963 assert_eq!(
964 native.supports(target),
965 native.targets.iter().any(|candidate| candidate == target)
966 );
967 }
968 }
969 }
970 }
971
972 #[test]
973 fn sharc_requires_every_header_and_license_at_the_official_root() {
974 let manifest = LockManifest::load().unwrap();
975 let artifact = &manifest.sdks.sharc.artifacts[0];
976 let temporary = tempfile::tempdir().unwrap();
977 let root = temporary.path().join("SHARC-commit");
978 fs::create_dir_all(root.join("include")).unwrap();
979
980 assert_eq!(artifact.required.len(), 6);
981
982 for relative in &artifact.required {
983 fs::write(root.join(relative), "fixture").unwrap();
984 }
985
986 assert_eq!(artifact.locate_root(temporary.path()).unwrap(), root);
987
988 artifact.validate_cached(&root).unwrap();
989
990 for relative in &artifact.required {
991 let path = root.join(relative);
992 fs::remove_file(&path).unwrap();
993
994 assert!(artifact.validate(&root).is_err());
995
996 fs::write(&path, "").unwrap();
997
998 assert!(artifact.validate(&root).is_err());
999
1000 fs::write(&path, "version https://git-lfs.github.com/spec/v1\n").unwrap();
1001
1002 assert!(artifact.validate(&root).is_err());
1003
1004 fs::write(&path, "fixture").unwrap();
1005 }
1006
1007 assert!(artifact.validate(&root.join("include")).is_err());
1008 }
1009
1010 #[test]
1011 fn source_artifacts_pin_the_complete_native_dependency_closure() {
1012 let manifest = LockManifest::load().unwrap();
1013
1014 for (sdk, targets) in [
1015 (
1016 Sdk::Nrd,
1017 &[
1018 "x86_64-unknown-linux-gnu",
1019 "x86_64-pc-windows-msvc",
1020 "aarch64-apple-darwin",
1021 ][..],
1022 ),
1023 (
1024 Sdk::Omm,
1025 &["x86_64-apple-darwin", "aarch64-apple-darwin"][..],
1026 ),
1027 ] {
1028 for target in targets {
1029 let artifact = manifest
1030 .sdks
1031 .get(sdk)
1032 .artifacts
1033 .iter()
1034 .find(|artifact| artifact.supports(target))
1035 .unwrap();
1036 let assembly = artifact.assembly.as_ref().unwrap();
1037
1038 assert_eq!(
1039 assembly.dependencies.len(),
1040 if sdk == Sdk::Nrd { 6 } else { 4 }
1041 );
1042 assert!(artifact.url.starts_with("https://codeload.github.com/"));
1043 assert_eq!(artifact.sha256.len(), 64);
1044
1045 for dependency in &assembly.dependencies {
1046 assert!(dependency.url.starts_with("https://codeload.github.com/"));
1047 assert_eq!(dependency.sha256.len(), 64);
1048 assert!(
1049 dependency
1050 .sha256
1051 .bytes()
1052 .all(|byte| byte.is_ascii_hexdigit())
1053 );
1054
1055 safe_relative_path(&dependency.root).unwrap();
1056 safe_relative_path(&dependency.destination).unwrap();
1057
1058 if let Some(files) = &dependency.files {
1059 assert!(!files.is_empty());
1060
1061 for file in files {
1062 safe_relative_path(file).unwrap();
1063 }
1064 }
1065 }
1066
1067 assert!(artifact.required.len() > 10);
1068 }
1069 }
1070 }
1071
1072 #[test]
1073 fn all_archive_hashes_are_valid_including_streamline() {
1074 let manifest = LockManifest::load().unwrap();
1075
1076 for sdk in [Sdk::Dlss, Sdk::Nrd, Sdk::Omm, Sdk::Sharc, Sdk::Streamline] {
1077 for artifact in &manifest.sdks.get(sdk).artifacts {
1078 assert_eq!(artifact.sha256.len(), 64);
1079 assert!(artifact.sha256.bytes().all(|byte| byte.is_ascii_hexdigit()));
1080 }
1081 }
1082
1083 assert_eq!(
1084 manifest.sdks.streamline.artifacts[0].sha256,
1085 "b7e4f31706cfacafba95d2d4abc2c9dcf2dc5fc58b2a6917c71f83051d271aa1"
1086 );
1087 }
1088
1089 #[test]
1090 fn source_recipe_changes_invalidate_cache_identity() {
1091 let mut artifact = LockManifest::load().unwrap().sdks.omm.artifacts.remove(2);
1092 let original = artifact.cache_key().unwrap();
1093 artifact.assembly.as_mut().unwrap().dependencies[0].sha256 = "0".repeat(64);
1094 let dependency_changed = artifact.cache_key().unwrap();
1095
1096 assert_ne!(original, dependency_changed);
1097
1098 artifact.assembly.as_mut().unwrap().patches[0]
1099 .after
1100 .push('\n');
1101
1102 assert_ne!(dependency_changed, artifact.cache_key().unwrap());
1103 }
1104
1105 #[test]
1106 fn assembles_and_patches_only_staging_then_requires_completion_stamp() {
1107 let temporary = tempfile::tempdir().unwrap();
1108 let override_root = temporary.path().join("override");
1109 let staging = temporary.path().join("staging");
1110 fs::create_dir_all(&override_root).unwrap();
1111 fs::create_dir_all(staging.join("upstream")).unwrap();
1112 fs::write(override_root.join("header.h"), "#elif __linux__\n").unwrap();
1113 fs::copy(
1114 override_root.join("header.h"),
1115 staging.join("upstream/header.h"),
1116 )
1117 .unwrap();
1118 let artifact: Artifact = toml::from_str(
1119 r##"
1120id = "fixture"
1121url = "https://example.invalid/fixture.zip"
1122sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1123targets = ["fixture"]
1124required = ["header.h"]
1125[assembly]
1126root = "fixture"
1127dependencies = []
1128[[assembly.relocations]]
1129source = "upstream/header.h"
1130destination = "header.h"
1131[[assembly.patches]]
1132path = "header.h"
1133before = "#elif __linux__"
1134after = "#elif defined(__linux__) || defined(__APPLE__)"
1135"##,
1136 )
1137 .unwrap();
1138 artifact.validate(&override_root).unwrap();
1140 artifact
1141 .assembly
1142 .as_ref()
1143 .unwrap()
1144 .prepare(&staging, temporary.path())
1145 .unwrap();
1146
1147 assert_eq!(
1148 fs::read_to_string(override_root.join("header.h")).unwrap(),
1149 "#elif __linux__\n"
1150 );
1151 assert!(
1152 fs::read_to_string(staging.join("header.h"))
1153 .unwrap()
1154 .contains("__APPLE__")
1155 );
1156 assert!(artifact.validate_cached(&staging).is_err());
1157
1158 fs::write(
1159 staging.join(".nvidia-sdk-source"),
1160 artifact.cache_key().unwrap(),
1161 )
1162 .unwrap();
1163 artifact.validate_cached(&staging).unwrap();
1164 fs::write(staging.join(".nvidia-sdk-source"), "stale").unwrap();
1165
1166 assert!(artifact.validate_cached(&staging).is_err());
1167 }
1168
1169 #[test]
1170 fn rejects_patch_drift_unsafe_relocations_and_wrong_hashes() {
1171 let temporary = tempfile::tempdir().unwrap();
1172 let path = temporary.path().join("header.h");
1173 fs::write(&path, "abc").unwrap();
1174 verify_hash(
1175 &path,
1176 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
1177 )
1178 .unwrap();
1179 let error = verify_hash(&path, &"0".repeat(64)).unwrap_err().to_string();
1180
1181 assert!(error.contains("archive hash mismatch"));
1182 assert!(error.contains(path.to_str().unwrap()));
1183
1184 let source_patch = SourcePatch {
1185 path: "header.h".into(),
1186 before: "missing".into(),
1187 after: "patched".into(),
1188 };
1189
1190 assert!(source_patch.apply(temporary.path()).is_err());
1191
1192 fs::write(&path, "missing missing").unwrap();
1193
1194 assert!(source_patch.apply(temporary.path()).is_err());
1195
1196 let relocation = SourceRelocation {
1197 source: "header.h".into(),
1198 destination: "../escaped".into(),
1199 };
1200
1201 assert!(
1202 relocation
1203 .apply(temporary.path(), temporary.path())
1204 .is_err()
1205 );
1206
1207 let relocation = SourceRelocation {
1208 source: "../escaped".into(),
1209 destination: "header.h".into(),
1210 };
1211
1212 assert!(
1213 relocation
1214 .apply(temporary.path(), temporary.path())
1215 .is_err()
1216 );
1217 }
1218
1219 #[test]
1220 fn file_and_zip_errors_name_operation_and_preserve_path_case() {
1221 let temporary = tempfile::tempdir().unwrap();
1222 let path = temporary.path().join("MixedCase.zip");
1223 let output = temporary.path().join("extracted");
1224 let error = verify_hash(&path, &"0".repeat(64)).unwrap_err().to_string();
1225
1226 assert!(error.contains("opening archive for hashing"));
1227 assert!(error.contains(path.to_str().unwrap()));
1228
1229 let error = extract_zip(&path, &output).unwrap_err().to_string();
1230
1231 assert!(error.contains("opening archive"));
1232 assert!(error.contains(path.to_str().unwrap()));
1233
1234 fs::write(&path, "not a zip archive").unwrap();
1235 let error = extract_zip(&path, &output).unwrap_err().to_string();
1236
1237 assert!(error.contains("reading zip archive"));
1238 assert!(error.contains(path.to_str().unwrap()));
1239 }
1240}