Skip to content

Commit

Permalink
Add Hostfxr::get_dotnet_environment_info
Browse files Browse the repository at this point in the history
  • Loading branch information
OpenByteDev committed Aug 13, 2023
1 parent 16be749 commit 61b385d
Show file tree
Hide file tree
Showing 4 changed files with 286 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ keywords = ["nethost", "hostfxr", "dotnet", "bindings", "coreclr"]
num_enum = { version = "0.6", default-features = false }
thiserror = { version = "1.0", default-features = false }
derive_more = { version = "0.99", features = ["deref", "from", "display"], default-features = false }
hostfxr-sys = { version = "0.9", features = ["enum-map"], default-features = false }
hostfxr-sys = { version = "0.9", features = ["enum-map", "undocumented"], default-features = false }
coreclr-hosting-shared = { version = "0.1", default-features = false }
destruct-drop = { version = "0.2", default-features = false }
ffi-opaque = { version = "2.0", default-features = false }
Expand Down
170 changes: 170 additions & 0 deletions src/hostfxr/library6_0.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use hostfxr_sys::hostfxr_dotnet_environment_info;

use crate::{
error::{HostingError, HostingResult},
hostfxr::Hostfxr,
pdcstring::PdCStr,
};
use std::{ffi::c_void, mem::MaybeUninit, path::PathBuf, ptr, slice};

/// Information about the current dotnet environment loaded using [Hostfxr::get_dotnet_environment_info].
#[derive(Debug, Clone)]
pub struct EnvironmentInfo {
/// Version of hostfxr used to load this info.
pub hostfxr_version: String,
/// Commit hash of hostfxr used to load this info.
pub hostfxr_commit_hash: String,
/// Currently installed sdks, ordered by version ascending.
pub sdks: Vec<SdkInfo>,
/// Currently installed frameworks, ordered by name and then version ascending.
pub frameworks: Vec<FrameworkInfo>,
}

impl PartialEq for EnvironmentInfo {
fn eq(&self, other: &Self) -> bool {
self.hostfxr_version == other.hostfxr_version
&& (self
.hostfxr_commit_hash
.starts_with(&other.hostfxr_commit_hash)
|| other
.hostfxr_commit_hash
.starts_with(&self.hostfxr_commit_hash))
&& self.sdks == other.sdks
&& self.frameworks == other.frameworks
}
}

impl Eq for EnvironmentInfo {}

impl PartialOrd for EnvironmentInfo {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.hostfxr_version.cmp(&other.hostfxr_version))
}
}

/// A struct representing an installed sdk.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SdkInfo {
/// The version of the sdk.
pub version: String,
/// The directory containing the sdk.
pub path: PathBuf,
}

impl PartialOrd for SdkInfo {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

impl Ord for SdkInfo {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.version.cmp(&other.version)
}
}

/// A struct representing an installed framework.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FrameworkInfo {
/// The name of the framework.
pub name: String,
/// The version of the framework.
pub version: String,
/// The directory containing the framework.
pub path: PathBuf,
}

impl PartialOrd for FrameworkInfo {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

impl Ord for FrameworkInfo {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.name
.cmp(&other.name)
.then_with(|| self.version.cmp(&other.version))
}
}

impl Hostfxr {
/// Loads info about the dotnet environemnt, including the version of hostfxr and installed sdks and frameworks.
///
/// # Ordering
/// SDks are ordered by version ascending and multi-level lookup locations are put before private locations - items later in the list have priority over items earlier in the list.
/// Frameworks are ordered by name ascending followed by version ascending. Multi-level lookup locations are put before private locations.
///
/// # Note
/// This is equivalent to the info retrieved using `dotnet --info`.
/// Which means it enumerates SDKs and frameworks from the dotnet root directory (either explicitly specified or using global install location per design).
/// If `DOTNET_MULTILEVEL_LOOKUP` is enabled (Windows-only), and the dotnet root is specified and it's not the global install location,
/// then it will also enumerate SDKs and frameworks from the global install location.
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "net6_0")))]
pub fn get_dotnet_environment_info(&self) -> Result<EnvironmentInfo, HostingError> {
let mut info = MaybeUninit::<EnvironmentInfo>::uninit();
let result = unsafe {
self.lib.hostfxr_get_dotnet_environment_info(
ptr::null(),
ptr::null_mut(),
get_dotnet_environment_info_callback,
info.as_mut_ptr().cast(),
)
};
HostingResult::from(result).into_result()?;
let info = unsafe { MaybeUninit::assume_init(info) };
Ok(info)
}
}

extern "C" fn get_dotnet_environment_info_callback(
info: *const hostfxr_dotnet_environment_info,
result_context: *mut c_void,
) {
let result = result_context.cast::<EnvironmentInfo>();

let raw_info = unsafe { &*info };
let hostfxr_version =
unsafe { PdCStr::from_str_ptr(raw_info.hostfxr_version) }.to_string_lossy();
let hostfxr_commit_hash =
unsafe { PdCStr::from_str_ptr(raw_info.hostfxr_commit_hash) }.to_string_lossy();

let raw_sdks = unsafe { slice::from_raw_parts(raw_info.sdks, raw_info.sdk_count) };
let sdks = raw_sdks
.iter()
.map(|raw_sdk| {
let version = unsafe { PdCStr::from_str_ptr(raw_sdk.version) }.to_string_lossy();
let path = unsafe { PdCStr::from_str_ptr(raw_sdk.path) }
.to_os_string()
.into();
SdkInfo { version, path }
})
.collect::<Vec<_>>();

let raw_frameworks =
unsafe { slice::from_raw_parts(raw_info.frameworks, raw_info.framework_count) };
let frameworks = raw_frameworks
.iter()
.map(|raw_framework| {
let name = unsafe { PdCStr::from_str_ptr(raw_framework.name) }.to_string_lossy();
let version = unsafe { PdCStr::from_str_ptr(raw_framework.version) }.to_string_lossy();
let path = unsafe { PdCStr::from_str_ptr(raw_framework.path) }
.to_os_string()
.into();
FrameworkInfo {
name,
version,
path,
}
})
.collect::<Vec<_>>();

let info = EnvironmentInfo {
hostfxr_version,
hostfxr_commit_hash,
sdks,
frameworks,
};

unsafe { result.write(info) };
}
6 changes: 6 additions & 0 deletions src/hostfxr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ mod library3_0;
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "netcore3_0")))]
pub use library3_0::*;

#[cfg(feature = "net6_0")]
mod library6_0;
#[cfg(feature = "net6_0")]
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "net6_0")))]
pub use library6_0::*;

#[cfg(feature = "netcore3_0")]
mod context;
#[cfg(feature = "netcore3_0")]
Expand Down
109 changes: 109 additions & 0 deletions tests/environment_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use netcorehost::{
hostfxr::{EnvironmentInfo, FrameworkInfo, SdkInfo},
nethost,
};
use std::{collections::HashMap, path::PathBuf, process::Command};

#[path = "common.rs"]
mod common;

#[test]
#[cfg(feature = "net6_0")]
fn get_dotnet_environment_info() {
let hostfxr = nethost::load_hostfxr().unwrap();

let actual_env = hostfxr.get_dotnet_environment_info().unwrap();
let expected_env = get_expected_environment_info();

assert_eq!(expected_env.hostfxr_version, actual_env.hostfxr_version);
assert_eq!(expected_env.sdks, actual_env.sdks);
assert_eq!(expected_env.frameworks, actual_env.frameworks);
}

fn get_expected_environment_info() -> EnvironmentInfo {
let output = Command::new("dotnet").arg("--info").output().unwrap();
assert!(output.status.success());
let output = String::from_utf8_lossy(&output.stdout);

let mut sections = Vec::new();
let mut current_section = None;
for line in output.lines() {
if line.is_empty() {
if let Some(section) = current_section.take() {
sections.push(section);
}
continue;
}

match &mut current_section {
None => current_section = Some((line.trim().trim_end_matches(':'), Vec::new())),
Some((_header, content)) => {
content.push(line.trim());
}
}
}

let host_section_content = sections
.iter()
.find(|(header, _content)| *header == "Host")
.map(|(_header, content)| content)
.unwrap();
let host_info = host_section_content
.iter()
.map(|line| {
let (key, value) = line.split_once(':').unwrap();
(key.trim(), value.trim())
})
.collect::<HashMap<_, _>>();
let hostfxr_version = host_info["Version"].to_string();
let hostfxr_commit_hash = host_info["Commit"].to_string();

let sdk_section_content = sections
.iter()
.find(|(header, _content)| *header == ".NET SDKs installed")
.map(|(_header, content)| content)
.unwrap();
let sdks = sdk_section_content
.iter()
.map(|line| {
let (version, enclosed_path) = line.split_once(' ').unwrap();
let path = enclosed_path.trim_start_matches('[').trim_end_matches(']');
let version = version.to_string();
let mut path = PathBuf::from(path);
path.push(&version);
SdkInfo { version, path }
})
.collect::<Vec<_>>();

let framework_section_content = sections
.iter()
.find(|(header, _content)| *header == ".NET runtimes installed")
.map(|(_header, content)| content)
.unwrap();
let frameworks = framework_section_content
.iter()
.map(|line| {
let mut items = line.splitn(3, ' ');
let name = items.next().unwrap();
let version = items.next().unwrap();
let enclosed_path = items.next().unwrap();
assert_eq!(items.next(), None);

let name = name.to_string();
let path = PathBuf::from(enclosed_path.trim_start_matches('[').trim_end_matches(']'));
let version = version.to_string();
FrameworkInfo {
name,
version,
path,
}
})
.collect::<Vec<_>>();

EnvironmentInfo {
hostfxr_version,
hostfxr_commit_hash,
sdks,
frameworks,
}
}

0 comments on commit 61b385d

Please sign in to comment.