1use crate::{
2 native_cargo::{cargo_target_directory, expand_cargo_target_directory},
3 FissionProject, NativeVariant,
4};
5use anyhow::{bail, Context, Result};
6use serde::{Deserialize, Serialize};
7use std::env;
8use std::fs;
9use std::path::{Component, Path, PathBuf};
10use std::process::Command;
11
12#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub struct NativeWindowsModuleConfig {
14 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub cargo_manifest_path: Option<String>,
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub cargo_package: Option<String>,
18 #[serde(default, skip_serializing_if = "Vec::is_empty")]
19 pub features: Vec<String>,
20 #[serde(default, skip_serializing_if = "is_false")]
21 pub no_default_features: bool,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub nuget_packages_config: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub nuget_packages_directory: Option<String>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub msbuild_project: Option<String>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub platform: Option<String>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub build_target: Option<String>,
32 #[serde(default, skip_serializing_if = "Vec::is_empty")]
33 pub test_binaries: Vec<String>,
34 #[serde(default, skip_serializing_if = "Vec::is_empty")]
35 pub products: Vec<NativeWindowsProductConfig>,
36}
37
38impl NativeWindowsModuleConfig {
39 pub fn is_empty(&self) -> bool {
40 self.cargo_manifest_path.is_none()
41 && self.cargo_package.is_none()
42 && self.features.is_empty()
43 && !self.no_default_features
44 && self.nuget_packages_config.is_none()
45 && self.nuget_packages_directory.is_none()
46 && self.msbuild_project.is_none()
47 && self.platform.is_none()
48 && self.build_target.is_none()
49 && self.test_binaries.is_empty()
50 && self.products.is_empty()
51 }
52}
53
54fn is_false(value: &bool) -> bool {
55 !*value
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "kebab-case")]
60pub enum NativeWindowsProductKind {
61 Runtime,
62 DriverPackage,
63}
64
65#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
66pub struct NativeWindowsProductConfig {
67 pub name: String,
68 pub path: String,
69 pub kind: NativeWindowsProductKind,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub destination: Option<String>,
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
75pub struct BuiltWindowsNativeProduct {
76 pub module: String,
77 pub name: String,
78 pub kind: NativeWindowsProductKind,
79 pub source: PathBuf,
80 pub destination: PathBuf,
81}
82
83pub fn build_windows_native_modules(
84 project_dir: &Path,
85 project: &FissionProject,
86 variant: Option<&NativeVariant>,
87 release: bool,
88) -> Result<Vec<BuiltWindowsNativeProduct>> {
89 let project_dir = canonical_project_dir(project_dir)?;
90 let configuration = if release { "Release" } else { "Debug" };
91 let mut products = Vec::new();
92
93 for module in project.native_modules_for_variant(variant) {
94 if module.windows.is_empty() {
95 continue;
96 }
97 let platform = optional_value(module.windows.platform.as_deref()).unwrap_or("x64");
98 let target_directory = match windows_build_system(&module.name, &module.windows)? {
99 WindowsBuildSystem::Cargo => Some(run_cargo_module_command(
100 &project_dir,
101 module.path.as_deref(),
102 &module.name,
103 &module.windows,
104 "build",
105 release,
106 )?),
107 WindowsBuildSystem::MsBuild => {
108 let native_tool_paths =
109 restore_windows_native_packages(&project_dir, &module.name, &module.windows)?;
110 let build_project = required_config_path(
111 &project_dir,
112 module.windows.msbuild_project.as_deref(),
113 &module.name,
114 )?;
115 let target =
116 optional_value(module.windows.build_target.as_deref()).unwrap_or("Build");
117 let mut command = Command::new("msbuild");
118 command
119 .arg(windows_external_tool_path(&build_project))
120 .arg("/m")
121 .arg(format!("/t:{target}"))
122 .arg(format!("/p:Configuration={configuration}"))
123 .arg(format!("/p:Platform={platform}"));
124 prepend_windows_native_tool_paths(&mut command, &native_tool_paths)?;
125 run_status(
126 &mut command,
127 &format!("Windows native module `{}`", module.name),
128 )?;
129 None
130 }
131 };
132
133 for product in &module.windows.products {
134 products.push(resolve_product(
135 &project_dir,
136 &module.name,
137 product,
138 configuration,
139 platform,
140 target_directory.as_deref(),
141 )?);
142 }
143 }
144
145 Ok(products)
146}
147
148fn restore_windows_native_packages(
149 project_dir: &Path,
150 module_name: &str,
151 config: &NativeWindowsModuleConfig,
152) -> Result<Vec<PathBuf>> {
153 let Some(packages_config) = optional_value(config.nuget_packages_config.as_deref()) else {
154 if config.nuget_packages_directory.is_some() {
155 bail!(
156 "Windows native module `{module_name}` sets `nuget_packages_directory` without `nuget_packages_config`"
157 );
158 }
159 return Ok(Vec::new());
160 };
161 let packages_config = resolve_project_path(project_dir, packages_config);
162 if !packages_config.is_file() {
163 bail!(
164 "Windows native module `{module_name}` NuGet packages config does not exist: {}",
165 packages_config.display()
166 );
167 }
168 let packages_directory = optional_value(config.nuget_packages_directory.as_deref())
169 .map(|path| resolve_project_path(project_dir, path))
170 .unwrap_or_else(|| {
171 packages_config
172 .parent()
173 .unwrap_or(project_dir)
174 .join("packages")
175 });
176 let mut command = Command::new("nuget");
177 command
178 .arg("restore")
179 .arg(windows_external_tool_path(&packages_config))
180 .arg("-PackagesDirectory")
181 .arg(windows_external_tool_path(&packages_directory))
182 .arg("-NonInteractive");
183 run_status(
184 &mut command,
185 &format!("Windows native module `{module_name}` NuGet restore"),
186 )?;
187
188 discover_windows_native_tool_paths(&packages_directory, env::consts::ARCH)
189}
190
191fn discover_windows_native_tool_paths(
192 packages_directory: &Path,
193 host_architecture: &str,
194) -> Result<Vec<PathBuf>> {
195 let host_architecture = match host_architecture {
196 "x86_64" => "x64",
197 "aarch64" => "ARM64",
198 _ => return Ok(Vec::new()),
199 };
200 let mut paths = Vec::new();
201
202 for package in read_directories(packages_directory)? {
203 let bin_root = package.join("c").join("bin");
204 if !bin_root.is_dir() {
205 continue;
206 }
207 for version in read_directories(&bin_root)? {
208 let candidate = version.join(host_architecture);
209 if candidate.is_dir() {
210 paths.push(candidate);
211 }
212 }
213 }
214
215 paths.sort();
216 paths.dedup();
217 Ok(paths)
218}
219
220fn read_directories(root: &Path) -> Result<Vec<PathBuf>> {
221 let mut paths = Vec::new();
222 for entry in fs::read_dir(root).with_context(|| {
223 format!(
224 "failed to inspect restored NuGet directory {}",
225 root.display()
226 )
227 })? {
228 let entry = entry?;
229 if entry.file_type()?.is_dir() {
230 paths.push(entry.path());
231 }
232 }
233 paths.sort();
234 Ok(paths)
235}
236
237fn prepend_windows_native_tool_paths(command: &mut Command, paths: &[PathBuf]) -> Result<()> {
238 if paths.is_empty() {
239 return Ok(());
240 }
241 let mut combined = paths
242 .iter()
243 .map(|path| windows_external_tool_path(path))
244 .collect::<Vec<_>>();
245 if let Some(current) = env::var_os("PATH") {
246 combined.extend(env::split_paths(¤t));
247 }
248 let joined = env::join_paths(combined)
249 .context("failed to construct PATH for Windows native module tools")?;
250 command.env("PATH", joined);
251 Ok(())
252}
253
254pub fn test_windows_native_modules(
255 project_dir: &Path,
256 project: &FissionProject,
257 variant: Option<&NativeVariant>,
258) -> Result<()> {
259 let products = build_windows_native_modules(project_dir, project, variant, false)?;
260 drop(products);
261 let project_dir = canonical_project_dir(project_dir)?;
262
263 for module in project.native_modules_for_variant(variant) {
264 if module.windows.is_empty() {
265 continue;
266 }
267 if windows_build_system(&module.name, &module.windows)? == WindowsBuildSystem::Cargo {
268 run_cargo_module_command(
269 &project_dir,
270 module.path.as_deref(),
271 &module.name,
272 &module.windows,
273 "test",
274 false,
275 )?;
276 continue;
277 }
278 let platform = optional_value(module.windows.platform.as_deref()).unwrap_or("x64");
279 for test_binary in &module.windows.test_binaries {
280 let test_binary = expand_path(test_binary, "Debug", platform, None, &module.name)?;
281 let test_binary = resolve_project_path(&project_dir, &test_binary);
282 if !test_binary.is_file() {
283 bail!(
284 "Windows native test binary for module `{}` does not exist: {}",
285 module.name,
286 test_binary.display()
287 );
288 }
289 let mut command = Command::new("vstest.console.exe");
290 command.arg(windows_external_tool_path(&test_binary));
291 run_status(
292 &mut command,
293 &format!("Windows native test binary `{}`", test_binary.display()),
294 )?;
295 }
296 }
297
298 Ok(())
299}
300
301#[derive(Clone, Copy, Debug, PartialEq, Eq)]
302enum WindowsBuildSystem {
303 Cargo,
304 MsBuild,
305}
306
307fn windows_build_system(
308 module_name: &str,
309 config: &NativeWindowsModuleConfig,
310) -> Result<WindowsBuildSystem> {
311 match (
312 optional_value(config.cargo_package.as_deref()),
313 optional_value(config.msbuild_project.as_deref()),
314 ) {
315 (Some(_), None) => {
316 if config.nuget_packages_config.is_some()
317 || config.nuget_packages_directory.is_some()
318 || config.build_target.is_some()
319 || !config.test_binaries.is_empty()
320 {
321 bail!(
322 "Windows native Cargo module `{module_name}` cannot set MSBuild, NuGet, or test-binary options"
323 );
324 }
325 Ok(WindowsBuildSystem::Cargo)
326 }
327 (None, Some(_)) => {
328 if config.cargo_manifest_path.is_some()
329 || !config.features.is_empty()
330 || config.no_default_features
331 {
332 bail!(
333 "Windows native MSBuild module `{module_name}` cannot set Cargo options"
334 );
335 }
336 Ok(WindowsBuildSystem::MsBuild)
337 }
338 (Some(_), Some(_)) => bail!(
339 "Windows native module `{module_name}` must select either `cargo_package` or `msbuild_project`, not both"
340 ),
341 (None, None) => bail!(
342 "Windows native module `{module_name}` requires `cargo_package` or `msbuild_project`"
343 ),
344 }
345}
346
347fn run_cargo_module_command(
348 project_dir: &Path,
349 module_path: Option<&str>,
350 module_name: &str,
351 config: &NativeWindowsModuleConfig,
352 cargo_command: &str,
353 release: bool,
354) -> Result<PathBuf> {
355 let package = optional_value(config.cargo_package.as_deref()).with_context(|| {
356 format!("Windows native module `{module_name}` requires `cargo_package`")
357 })?;
358 let manifest = resolve_cargo_manifest_path(
359 project_dir,
360 module_path,
361 config.cargo_manifest_path.as_deref(),
362 module_name,
363 )?;
364 let mut command = Command::new("cargo");
365 command
366 .arg(cargo_command)
367 .arg("--manifest-path")
368 .arg(&manifest)
369 .arg("--package")
370 .arg(package)
371 .current_dir(project_dir);
372 if release && cargo_command == "build" {
373 command.arg("--release");
374 }
375 if config.no_default_features {
376 command.arg("--no-default-features");
377 }
378 if !config.features.is_empty() {
379 let features = config
380 .features
381 .iter()
382 .map(|feature| required_value(feature, "Windows native Cargo feature"))
383 .collect::<Result<Vec<_>>>()?
384 .join(",");
385 command.arg("--features").arg(features);
386 }
387 run_status(
388 &mut command,
389 &format!("Windows native module `{module_name}` Cargo {cargo_command}"),
390 )?;
391 cargo_target_directory(project_dir, &manifest, module_name, "Windows")
392}
393
394fn resolve_cargo_manifest_path(
395 project_dir: &Path,
396 module_path: Option<&str>,
397 configured: Option<&str>,
398 module_name: &str,
399) -> Result<PathBuf> {
400 let manifest = if let Some(configured) = optional_value(configured) {
401 resolve_project_path(project_dir, configured)
402 } else if let Some(module_path) = optional_value(module_path) {
403 resolve_project_path(project_dir, module_path).join("Cargo.toml")
404 } else {
405 project_dir.join("Cargo.toml")
406 };
407 if !manifest.is_file() {
408 bail!(
409 "Windows native Cargo manifest for module `{module_name}` does not exist: {}",
410 manifest.display()
411 );
412 }
413 Ok(manifest)
414}
415
416pub fn stage_windows_runtime_products(
417 destination_root: &Path,
418 products: &[BuiltWindowsNativeProduct],
419) -> Result<()> {
420 for product in products {
421 if product.kind != NativeWindowsProductKind::Runtime {
422 continue;
423 }
424 let destination = destination_root.join(&product.destination);
425 if destination.exists() {
426 bail!(
427 "Windows native runtime product `{}` would overwrite {}",
428 product.name,
429 destination.display()
430 );
431 }
432 copy_product(&product.source, &destination)?;
433 }
434 Ok(())
435}
436
437fn required_config_path(
438 project_dir: &Path,
439 configured: Option<&str>,
440 module_name: &str,
441) -> Result<PathBuf> {
442 let configured = optional_value(configured).with_context(|| {
443 format!("Windows native module `{module_name}` requires `msbuild_project`")
444 })?;
445 let path = resolve_project_path(project_dir, configured);
446 if !path.is_file() {
447 bail!(
448 "Windows native module `{module_name}` MSBuild project does not exist: {}",
449 path.display()
450 );
451 }
452 Ok(path)
453}
454
455fn resolve_product(
456 project_dir: &Path,
457 module_name: &str,
458 product: &NativeWindowsProductConfig,
459 configuration: &str,
460 platform: &str,
461 cargo_target_directory: Option<&Path>,
462) -> Result<BuiltWindowsNativeProduct> {
463 let name = required_value(&product.name, "Windows native product name")?;
464 let path = required_value(&product.path, "Windows native product path")?;
465 let expanded = expand_path(
466 path,
467 configuration,
468 platform,
469 cargo_target_directory,
470 module_name,
471 )?;
472 let source = resolve_project_path(project_dir, &expanded);
473 if !source.exists() {
474 bail!(
475 "Windows native product `{name}` from module `{module_name}` does not exist: {}",
476 source.display()
477 );
478 }
479 let default_destination = source
480 .file_name()
481 .map(PathBuf::from)
482 .context("Windows native product source has no file name")?;
483 let destination = product
484 .destination
485 .as_deref()
486 .map(str::trim)
487 .filter(|value| !value.is_empty())
488 .map(PathBuf::from)
489 .unwrap_or(default_destination);
490 validate_relative_destination(&destination)?;
491
492 Ok(BuiltWindowsNativeProduct {
493 module: module_name.to_string(),
494 name: name.to_string(),
495 kind: product.kind,
496 source,
497 destination,
498 })
499}
500
501fn validate_relative_destination(destination: &Path) -> Result<()> {
502 if destination.as_os_str().is_empty() || destination.is_absolute() {
503 bail!("Windows native product destination must be a non-empty relative path");
504 }
505 if destination.components().any(|component| {
506 matches!(
507 component,
508 Component::ParentDir | Component::RootDir | Component::Prefix(_)
509 )
510 }) {
511 bail!(
512 "Windows native product destination cannot escape the application or installer root: {}",
513 destination.display()
514 );
515 }
516 Ok(())
517}
518
519fn expand_path(
520 value: &str,
521 configuration: &str,
522 platform: &str,
523 cargo_target_directory: Option<&Path>,
524 module_name: &str,
525) -> Result<String> {
526 let value = value
527 .replace("{configuration}", configuration)
528 .replace("{profile}", &configuration.to_ascii_lowercase())
529 .replace("{platform}", platform);
530 expand_cargo_target_directory(&value, cargo_target_directory, module_name, "Windows")
531}
532
533fn copy_product(source: &Path, destination: &Path) -> Result<()> {
534 if source.is_dir() {
535 fs::create_dir_all(destination)?;
536 for entry in fs::read_dir(source)? {
537 let entry = entry?;
538 copy_product(&entry.path(), &destination.join(entry.file_name()))?;
539 }
540 return Ok(());
541 }
542 let parent = destination
543 .parent()
544 .context("Windows native product destination has no parent")?;
545 fs::create_dir_all(parent)?;
546 fs::copy(source, destination).with_context(|| {
547 format!(
548 "failed to copy Windows native product {} to {}",
549 source.display(),
550 destination.display()
551 )
552 })?;
553 Ok(())
554}
555
556fn resolve_project_path(project_dir: &Path, value: &str) -> PathBuf {
557 let path = Path::new(value);
558 if path.is_absolute() {
559 path.to_path_buf()
560 } else {
561 project_dir.join(path)
562 }
563}
564
565fn canonical_project_dir(project_dir: &Path) -> Result<PathBuf> {
566 fs::canonicalize(project_dir).with_context(|| {
567 format!(
568 "failed to resolve project directory {}",
569 project_dir.display()
570 )
571 })
572}
573
574fn windows_external_tool_path(path: &Path) -> PathBuf {
575 #[cfg(windows)]
576 {
577 use std::ffi::OsString;
578 use std::os::windows::ffi::{OsStrExt, OsStringExt};
579
580 let encoded = path.as_os_str().encode_wide().collect::<Vec<_>>();
581 return normalize_windows_verbatim_units(&encoded)
582 .map(|units| PathBuf::from(OsString::from_wide(&units)))
583 .unwrap_or_else(|| path.to_path_buf());
584 }
585
586 #[cfg(not(windows))]
587 path.to_path_buf()
588}
589
590#[cfg(any(windows, test))]
591fn normalize_windows_verbatim_units(path: &[u16]) -> Option<Vec<u16>> {
592 const VERBATIM_PREFIX: &[u16] = &[b'\\' as u16, b'\\' as u16, b'?' as u16, b'\\' as u16];
593 const VERBATIM_UNC_PREFIX: &[u16] = &[
594 b'\\' as u16,
595 b'\\' as u16,
596 b'?' as u16,
597 b'\\' as u16,
598 b'U' as u16,
599 b'N' as u16,
600 b'C' as u16,
601 b'\\' as u16,
602 ];
603
604 if let Some(remainder) = path.strip_prefix(VERBATIM_UNC_PREFIX) {
605 let mut normalized = vec![b'\\' as u16, b'\\' as u16];
606 normalized.extend_from_slice(remainder);
607 return Some(normalized);
608 }
609 path.strip_prefix(VERBATIM_PREFIX).map(Vec::from)
610}
611
612fn required_value<'a>(value: &'a str, label: &str) -> Result<&'a str> {
613 let value = value.trim();
614 if value.is_empty() {
615 bail!("{label} cannot be empty");
616 }
617 Ok(value)
618}
619
620fn optional_value(value: Option<&str>) -> Option<&str> {
621 value.map(str::trim).filter(|value| !value.is_empty())
622}
623
624fn run_status(command: &mut Command, label: &str) -> Result<()> {
625 let status = command
626 .status()
627 .with_context(|| format!("failed to run {label}"))?;
628 if !status.success() {
629 bail!("{label} failed with {status}");
630 }
631 Ok(())
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637 use crate::FissionProject;
638
639 #[test]
640 fn parses_windows_native_products() {
641 let project: FissionProject = toml::from_str(
642 r#"
643targets = ["windows"]
644
645[app]
646name = "demo"
647app_id = "com.example.demo"
648
649[[native.modules]]
650name = "demo-native"
651
652[native.modules.windows]
653nuget_packages_config = "platforms/windows/native/packages.config"
654nuget_packages_directory = "platforms/windows/native/packages"
655msbuild_project = "platforms/windows/native/Demo.sln"
656platform = "x64"
657test_binaries = ["platforms/windows/native/{platform}/{configuration}/DemoTests.exe"]
658
659[[native.modules.windows.products]]
660name = "provider"
661path = "platforms/windows/native/{platform}/{configuration}/Provider.dll"
662kind = "runtime"
663destination = "native/Provider.dll"
664
665[[native.modules.windows.products]]
666name = "minifilter"
667path = "platforms/windows/native/{platform}/{configuration}/DriverPackage"
668kind = "driver-package"
669"#,
670 )
671 .unwrap();
672
673 let module = &project.native.modules[0].windows;
674 assert_eq!(module.platform.as_deref(), Some("x64"));
675 assert_eq!(
676 module.nuget_packages_config.as_deref(),
677 Some("platforms/windows/native/packages.config")
678 );
679 assert_eq!(module.products.len(), 2);
680 assert_eq!(
681 module.products[1].kind,
682 NativeWindowsProductKind::DriverPackage
683 );
684 }
685
686 #[test]
687 fn parses_windows_cargo_native_products() {
688 let project: FissionProject = toml::from_str(
689 r#"
690targets = ["windows"]
691
692[app]
693name = "demo"
694app_id = "com.example.demo"
695
696[[native.modules]]
697name = "demo-helper"
698path = "../demo-helper"
699
700[native.modules.windows]
701cargo_package = "demo-helper"
702features = ["installer"]
703no_default_features = true
704
705[[native.modules.windows.products]]
706name = "helper"
707path = "../../target/{profile}/demo-helper.exe"
708kind = "runtime"
709destination = "tools/demo-helper.exe"
710"#,
711 )
712 .unwrap();
713
714 let module = &project.native.modules[0].windows;
715 assert_eq!(module.cargo_package.as_deref(), Some("demo-helper"));
716 assert_eq!(module.features, ["installer"]);
717 assert!(module.no_default_features);
718 assert_eq!(
719 windows_build_system("demo-helper", module).unwrap(),
720 WindowsBuildSystem::Cargo
721 );
722 }
723
724 #[test]
725 fn rejects_ambiguous_windows_native_build_systems() {
726 let config = NativeWindowsModuleConfig {
727 cargo_package: Some("demo".into()),
728 msbuild_project: Some("Demo.sln".into()),
729 ..Default::default()
730 };
731 let error = windows_build_system("demo", &config).unwrap_err();
732 assert!(error
733 .to_string()
734 .contains("either `cargo_package` or `msbuild_project`"));
735 }
736
737 #[test]
738 fn expands_configuration_and_platform_tokens() {
739 assert_eq!(
740 expand_path(
741 "{cargo_target_dir}/{platform}/{configuration}/{profile}",
742 "Release",
743 "ARM64",
744 Some(Path::new("C:/shared/cargo")),
745 "demo-native",
746 )
747 .unwrap(),
748 "C:/shared/cargo/ARM64/Release/release"
749 );
750 }
751
752 #[test]
753 fn rejects_destination_traversal() {
754 let error = validate_relative_destination(Path::new("../driver.sys")).unwrap_err();
755 assert!(error.to_string().contains("cannot escape"));
756 }
757
758 #[test]
759 fn stages_runtime_products_without_driver_packages() {
760 let root = unique_dir("windows-native-stage");
761 let source = root.join("source");
762 let destination = root.join("destination");
763 fs::create_dir_all(&source).unwrap();
764 fs::write(source.join("provider.dll"), b"runtime").unwrap();
765 fs::write(source.join("driver.sys"), b"driver").unwrap();
766 let products = vec![
767 BuiltWindowsNativeProduct {
768 module: "demo".into(),
769 name: "provider".into(),
770 kind: NativeWindowsProductKind::Runtime,
771 source: source.join("provider.dll"),
772 destination: PathBuf::from("native/provider.dll"),
773 },
774 BuiltWindowsNativeProduct {
775 module: "demo".into(),
776 name: "driver".into(),
777 kind: NativeWindowsProductKind::DriverPackage,
778 source: source.join("driver.sys"),
779 destination: PathBuf::from("driver/driver.sys"),
780 },
781 ];
782
783 stage_windows_runtime_products(&destination, &products).unwrap();
784
785 assert_eq!(
786 fs::read(destination.join("native/provider.dll")).unwrap(),
787 b"runtime"
788 );
789 assert!(!destination.join("driver/driver.sys").exists());
790 }
791
792 #[test]
793 fn discovers_restored_wdk_tools_for_host_architecture() {
794 let root = unique_dir("windows-native-tools");
795 let x64 = root
796 .join("Microsoft.Windows.WDK.x64.10.0.1")
797 .join("c/bin/10.0.1/x64");
798 let arm64 = root
799 .join("Microsoft.Windows.WDK.x64.10.0.1")
800 .join("c/bin/10.0.1/ARM64");
801 fs::create_dir_all(&x64).unwrap();
802 fs::create_dir_all(&arm64).unwrap();
803 fs::write(x64.join("stampinf.exe"), b"fixture").unwrap();
804
805 let paths = discover_windows_native_tool_paths(&root, "x86_64").unwrap();
806
807 assert_eq!(paths, vec![x64]);
808 }
809
810 #[test]
811 fn ignores_restored_packages_without_native_host_tools() {
812 let root = unique_dir("windows-native-no-tools");
813 fs::create_dir_all(root.join("Example.Package.1.0.0/lib/net8.0")).unwrap();
814
815 let paths = discover_windows_native_tool_paths(&root, "x86_64").unwrap();
816
817 assert!(paths.is_empty());
818 }
819
820 #[test]
821 fn normalizes_verbatim_drive_path_for_windows_tools() {
822 let path = r"\\?\D:\a\demo\packages".encode_utf16().collect::<Vec<_>>();
823 let normalized = normalize_windows_verbatim_units(&path).unwrap();
824
825 assert_eq!(
826 String::from_utf16(normalized.as_slice()).unwrap(),
827 r"D:\a\demo\packages"
828 );
829 }
830
831 #[test]
832 fn normalizes_verbatim_unc_path_for_windows_tools() {
833 let path = r"\\?\UNC\server\share\packages"
834 .encode_utf16()
835 .collect::<Vec<_>>();
836 let normalized = normalize_windows_verbatim_units(&path).unwrap();
837
838 assert_eq!(
839 String::from_utf16(normalized.as_slice()).unwrap(),
840 r"\\server\share\packages"
841 );
842 }
843
844 #[test]
845 fn leaves_non_verbatim_windows_path_unchanged() {
846 let path = r"D:\a\demo\packages".encode_utf16().collect::<Vec<_>>();
847
848 assert!(normalize_windows_verbatim_units(&path).is_none());
849 }
850
851 fn unique_dir(label: &str) -> PathBuf {
852 let path = std::env::temp_dir().join(format!(
853 "fission-{label}-{}-{}",
854 std::process::id(),
855 std::time::SystemTime::now()
856 .duration_since(std::time::UNIX_EPOCH)
857 .unwrap()
858 .as_nanos()
859 ));
860 fs::create_dir_all(&path).unwrap();
861 path
862 }
863}