Skip to content

Commit

Permalink
Install natively using dotnet-install scripts (#2)
Browse files Browse the repository at this point in the history
* Install natively using dotnet-install scripts

Having an installation for each .NET SDK version causes all sorts of
issues, as the whole ecosystem expects all SDKs and runtimes to share
the same installation directory.

In practice this is currently causing issues because DOTNET_ROOT cannot
be set to anything meaningful on a system-level. Other tooling like
IntelliJ Rider can thus not find the correct SDK to use. You'd have to
somehow set DOTNET_ROOT per project, and even then that project can only
be aware of a single SDK version at a time.

So I've decided to rely on proto's `native_install` feature instead,
which opens another can of worms, but a more manageable one at least.
It relies on the official dotnet-install scripts to install SDKs into
whatever DOTNET_ROOT is set to (or ~/.dotnet per default).
Supporting uninstallation will come later, as it will be a bit tricky,
now that everything lives in the same directory.

The dotnet executable will no longer be shimmed nor symlinked, and the
user is expected to add DOTNET_ROOT to their PATH.

* Remove obsolete tests
  • Loading branch information
Phault authored Feb 14, 2024
1 parent c9164db commit 9986b03
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 175 deletions.
17 changes: 17 additions & 0 deletions src/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use std::path::PathBuf;

use extism_pdk::*;
use proto_pdk::{host_env, HostEnvironment};

#[host_fn]
extern "ExtismHost" {
fn get_env_var(name: String) -> String;
}

pub fn get_dotnet_root(env: &HostEnvironment) -> Result<PathBuf, Error> {
// Variable returns a real path
Ok(host_env!("DOTNET_ROOT")
.map(PathBuf::from)
// So we need our fallback to also be a real path
.unwrap_or_else(|| env.home_dir.real_path().join(".dotnet")))
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#[cfg(feature = "wasm")]
mod global_json;
#[cfg(feature = "wasm")]
mod helpers;
#[cfg(feature = "wasm")]
mod proto;
#[cfg(feature = "wasm")]
mod release_index;
Expand Down
201 changes: 79 additions & 122 deletions src/proto.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use std::collections::HashMap;
use std::{fs, path::PathBuf};

use extism_pdk::*;
use proto_pdk::*;

use crate::{
global_json::GlobalJson,
release_index::{fetch_channel_releases, fetch_release_index},
global_json::GlobalJson, helpers::get_dotnet_root, release_index::fetch_release_index,
};

#[host_fn]
extern "ExtismHost" {
fn exec_command(input: Json<ExecCommandInput>) -> Json<ExecCommandOutput>;
}

static NAME: &str = ".NET";
static BIN: &str = "dotnet";

Expand All @@ -17,6 +21,11 @@ pub fn register_tool(Json(_): Json<ToolMetadataInput>) -> FnResult<Json<ToolMeta
name: NAME.into(),
type_of: PluginType::Language,
plugin_version: Some(env!("CARGO_PKG_VERSION").into()),
inventory: ToolInventoryMetadata {
// we'll stream the output from the dotnet-install script instead
disable_progress_bars: true,
..Default::default()
},
..ToolMetadataOutput::default()
}))
}
Expand Down Expand Up @@ -107,140 +116,88 @@ pub fn parse_version_file(
}

#[plugin_fn]
pub fn download_prebuilt(
Json(input): Json<DownloadPrebuiltInput>,
) -> FnResult<Json<DownloadPrebuiltOutput>> {
pub fn native_install(
Json(input): Json<NativeInstallInput>,
) -> FnResult<Json<NativeInstallOutput>> {
let env = get_host_environment()?;
check_supported_os_and_arch(
NAME,
&env,
permutations! [
HostOS::Linux => [
HostArch::X86, HostArch::X64, HostArch::Arm, HostArch::Arm64
],
HostOS::MacOS => [HostArch::X64, HostArch::Arm64],
HostOS::Windows => [HostArch::X86, HostArch::X64, HostArch::Arm64],
],
)?;

let version = match input.context.version {
VersionSpec::Canary => Err(plugin_err!(PluginError::UnsupportedCanary {
tool: NAME.into(),
})),
VersionSpec::Version(v) => Ok(v),
VersionSpec::Alias(alias) => {
// uncertain if this code ever runs, as Proto will seemingly always resolve aliases via resolve_version first?

let releases_index = fetch_release_index()?;

let channel = match alias.to_lowercase().as_str() {
"latest" => releases_index.first(),
"lts" | "sts" => releases_index
.iter()
.find(|x| x.release_type.eq_ignore_ascii_case(&alias)),
_ => None,
};

match channel {
Some(c) => Version::parse(&c.latest_sdk).map_err(|e| plugin_err!(e)),
None => Err(plugin_err!(PluginError::Message(format!(
"Alias '{alias}' is not supported"
)))),
}
}
}?;

let channel_version = format!("{}.{}", version.major, version.minor);
let releases = fetch_channel_releases(&channel_version)?;
let version = &input.context.version;

let sdk = releases
.iter()
.flat_map(|release| {
release
.sdks
.to_owned()
.unwrap_or(vec![release.sdk.to_owned()])
})
.find(|sdk| version.to_string().eq(&sdk.version));

if sdk.is_none() {
return Err(plugin_err!(PluginError::Message(
"Failed to find release matching '{version}'".into()
)));
let is_windows = env.os.is_windows();
let script_path = PathBuf::from("/proto/temp").join(if is_windows {
"dotnet-install.ps1"
} else {
"dotnet-install.sh"
});

if !script_path.exists() {
fs::write(
&script_path,
fetch(
HttpRequest::new(if is_windows {
"https://dot.net/v1/dotnet-install.ps1"
} else {
"https://dot.net/v1/dotnet-install.sh"
}),
None,
)?
.body(),
)?;
}

let arch = match env.arch {
HostArch::X86 => "x86",
HostArch::X64 => "x64",
HostArch::Arm => "arm",
HostArch::Arm64 => "arm64",
_ => unreachable!(),
};

let os = match env.os {
HostOS::Linux => {
if is_musl(&env) {
"linux-musl"
} else {
"linux"
}
let command_output = exec_command!(
input,
ExecCommandInput {
command: script_path.to_string_lossy().to_string(),
args: vec![
"-Version".into(),
version.to_string(),
"-InstallDir".into(),
get_dotnet_root(&env)?
.to_str()
.ok_or(anyhow!("unable to deduce installation dir"))?
.to_owned(),
"-NoPath".into()
],
set_executable: true,
stream: true,
..ExecCommandInput::default()
}
HostOS::MacOS => "osx",
HostOS::Windows => "win",
_ => unreachable!(),
};
);

let rid = format!("{os}-{arch}");

let file_ext = match env.os {
HostOS::Windows => ".zip",
_ => ".tar.gz",
};

let sdk = sdk.unwrap();
let file = sdk.files.iter().find(|f| {
f.rid.as_ref().is_some_and(|file_rid| file_rid.eq(&rid)) && f.name.ends_with(&file_ext)
});
Ok(Json(NativeInstallOutput {
installed: command_output.exit_code == 0,
..NativeInstallOutput::default()
}))
}

match file {
Some(file) => Ok(Json(DownloadPrebuiltOutput {
download_url: file.url.to_owned(),
download_name: file.name.to_owned().into(),
..DownloadPrebuiltOutput::default()
})),
None => Err(plugin_err!(PluginError::Message(format!(
"Unable to install {NAME}, unable to find build fitting {rid}."
)))),
}
#[plugin_fn]
pub fn native_uninstall(
Json(_input): Json<NativeUninstallInput>,
) -> FnResult<Json<NativeUninstallOutput>> {
warn!("Uninstalling .NET sdks is not currently supported, as they all share their installation folder.");

Ok(Json(NativeUninstallOutput {
uninstalled: false,
..NativeUninstallOutput::default()
}))
}

#[plugin_fn]
pub fn locate_executables(
Json(input): Json<LocateExecutablesInput>,
Json(_input): Json<LocateExecutablesInput>,
) -> FnResult<Json<LocateExecutablesOutput>> {
let env = get_host_environment()?;
let tool_dir = input.context.tool_dir.real_path();

let exe_name = env.os.get_exe_name(BIN);
let mut primary = ExecutableConfig::new(&exe_name);
primary.shim_env_vars = Some(HashMap::from_iter([
(
"DOTNET_ROOT".into(),
tool_dir
.clone()
.into_os_string()
.into_string()
.unwrap_or_default(),
),
(
"DOTNET_HOST_PATH".into(),
tool_dir
.join(&exe_name)
.into_os_string()
.into_string()
.unwrap_or_default(),
),
]));
let mut primary = ExecutableConfig::new(
&get_dotnet_root(&env)?
.join(&exe_name)
.into_os_string()
.into_string()
.map_err(|_| anyhow!("unable to build path to the dotnet binary"))?,
);
primary.no_bin = true;
primary.no_shim = true;

Ok(Json(LocateExecutablesOutput {
primary: Some(primary),
Expand Down
12 changes: 0 additions & 12 deletions src/release_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,3 @@ pub fn fetch_release_index() -> Result<Vec<DotnetReleasesIndex>, Error> {
.map(|r: DotnetReleasesIndexJson| r.releases_index)
.map_err(|e| e.context(format!("Failed to retrieve index of releases")))
}

pub fn fetch_channel_releases(channel_version: &str) -> Result<Vec<DotnetRelease>, Error> {
fetch_url(format!(
"https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/{channel_version}/releases.json"
))
.map(|r: DotnetReleasesJson| r.releases)
.map_err(|e| {
e.context(format!(
"Failed to retrieve releases for channel '{channel_version}'"
))
})
}
3 changes: 0 additions & 3 deletions tests/download_test.rs

This file was deleted.

18 changes: 0 additions & 18 deletions tests/metadata_test.rs

This file was deleted.

20 changes: 0 additions & 20 deletions tests/shims_test.rs

This file was deleted.

0 comments on commit 9986b03

Please sign in to comment.