Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support asdf plugins through a proto WASM plugin #540

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions crates/asdf-plugin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "asdf-plugin"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
crate-type = ['cdylib']

[dependencies]
proto_pdk = { path = "../../crates/pdk" }
proto_core = { version = "0.38.2", path = "../core" }
extism-pdk = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
dirs = "5.0.1"

[dev-dependencies]
proto_pdk_test_utils = { path = "../../crates/pdk-test-utils" }
starbase_sandbox = "*"
tokio = { workspace = true }
226 changes: 226 additions & 0 deletions crates/asdf-plugin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
use dirs;
use extism_pdk::*;
use proto_core::Tool;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plugins shouldn't use proto_core at all. Refer to the other plugins as a reference: https://github.com/moonrepo/node-plugin

use proto_pdk::*;
use serde::Deserialize;
use std::env;
use std::path::Path;

#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields, rename_all = "kebab-case")]
pub struct AsdfConfig {
pub asdf_plugin: Option<String>,
pub asdf_repository: Option<String>,
}

pub struct AsdfPlugin {
pub tool: Tool,
}

impl AsdfPlugin {
pub fn detect_version_files(&self) -> DetectVersionOutput {
DetectVersionOutput {
files: vec![".tool-versions".into()],
ignore: vec![],
}
}

pub fn parse_version_file(&self, input: ParseVersionFileInput) -> ParseVersionFileOutput {
let mut version = None;
if input.file == ".tool-versions" {
for line in input.content.lines() {
if let Some((tool, version_str)) = line.split_once(' ') {
if tool == self.tool.get_name() {
version = Some(UnresolvedVersionSpec::parse(version_str.trim()).unwrap());
break;
}
}
}
}
ParseVersionFileOutput { version }
}

pub fn download_prebuilt(&self, input: DownloadPrebuiltInput) -> DownloadPrebuiltOutput {
let env = get_host_environment().unwrap();

check_supported_os_and_arch(
"ASDF Plugin",
&env,
permutations![
HostOS::Linux => [HostArch::X64, HostArch::Arm64, HostArch::Arm, HostArch::Powerpc64, HostArch::S390x],
HostOS::MacOS => [HostArch::X64, HostArch::Arm64],
HostOS::Windows => [HostArch::X64, HostArch::X86, HostArch::Arm64],
],
).unwrap();

let version = input.context.version;
let arch = env.arch;
let os = env.os;

let prefix = match os {
HostOS::Linux => format!("asdf-plugin-v{version}-linux-{arch}"),
HostOS::MacOS => format!("asdf-plugin-v{version}-darwin-{arch}"),
HostOS::Windows => format!("asdf-plugin-v{version}-win-{arch}"),
other => {
return DownloadPrebuiltOutput {
download_url: format!("Unsupported platform: {}", other),
..DownloadPrebuiltOutput::default()
};
}
};

let filename = if os == HostOS::Windows {
format!("{prefix}.zip")
} else {
format!("{prefix}.tar.xz")
};

let config: AsdfConfig = self.tool.config();
let asdf_plugin = config
.asdf_plugin
.unwrap_or_else(|| self.tool.get_name().to_string());
let repository = config
.asdf_repository
.unwrap_or_else(|| format!("https://github.com/asdf-vm/asdf-{}.git", asdf_plugin));

DownloadPrebuiltOutput {
archive_prefix: Some(prefix),
download_url: format!("{repository}/releases/download/v{version}/{filename}"),
download_name: Some(filename),
checksum_url: Some(format!(
"{repository}/releases/download/v{version}/SHA256SUMS"
)),
checksum_public_key: Some("public-key-string".into()), // Need to adjust if applicable
..DownloadPrebuiltOutput::default()
}
}

pub fn install_plugin(&self, repository: &str) {
let asdf_dir = match env::var("ASDF_DATA_DIR") {
Ok(val) => Path::new(&val).to_path_buf(),
Err(_) => dirs::home_dir().unwrap().join(".asdf"),
};

let plugin_dir = asdf_dir.join("plugins").join(self.tool.get_name());

if !plugin_dir.exists() {
std::fs::create_dir_all(&plugin_dir).unwrap();
}

std::process::Command::new("git")
.arg("clone")
.arg(repository)
.arg(&plugin_dir)
.output()
.expect("Failed to clone asdf plugin");
}

pub fn pre_install(&self, mut input: InstallHook) {
let config: AsdfConfig = self.tool.config();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had confusion here about referencing config. Any guidance on this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let repository = config.asdf_repository.unwrap_or_else(|| {
format!(
"https://github.com/asdf-vm/asdf-{}.git",
self.tool.get_name()
)
});
self.install_plugin(&repository);
input.context = self.prepare_context(input.context);
self.tool
.plugin
.call_func_without_output("pre_install", input)
.unwrap();
}

fn prepare_context(&self, context: ToolContext) -> ToolContext {
let dir = if context.tool_dir.any_path().components().count() == 0 {
self.tool.get_product_dir()
} else {
context.tool_dir.any_path().to_path_buf()
};
ToolContext {
tool_dir: self.tool.to_virtual_path(&dir),
..context
}
}
}

// register_tool: Registers the plugin and provides metadata.
#[plugin_fn]
pub fn register_tool(Json(input): Json<ToolMetadataInput>) -> FnResult<Json<ToolMetadataOutput>> {
Ok(Json(ToolMetadataOutput {
name: "ASDF Plugin".into(),
type_of: PluginType::Language,
plugin_version: Some(env!("CARGO_PKG_VERSION").into()),
..ToolMetadataOutput::default()
}))
}

// download_prebuilt: Handles downloading the pre-built tool, with URL construction based on OS and architecture.
#[plugin_fn]
pub fn download_prebuilt(
Json(input): Json<DownloadPrebuiltInput>,
) -> FnResult<Json<DownloadPrebuiltOutput>> {
let asdf_plugin = AsdfPlugin {
tool: Tool::default(),
};
Ok(Json(asdf_plugin.download_prebuilt(input)))
}

// unpack_archive: Unpacks downloaded archives based on their file extension.
#[plugin_fn]
pub fn unpack_archive(Json(input): Json<UnpackArchiveInput>) -> FnResult<()> {
let input_file = input.input_file;
let output_dir = input.output_dir;

// Need to ensure file type and unpack accordingly
if input_file.ends_with(".tar.xz") {
//TODO: Implement the untar and unzip moments
// untar(input_file, output_dir)?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this untar and unzip could be custom implemented or any suggested crates for it?

} else if input_file.ends_with(".zip") {
// unzip(input_file, output_dir)?;
} else {
return Err(PluginError::UnsupportedArchiveFormat(format!(
"Unsupported archive format: {}",
input_file
))
.into());
}

Ok(())
}

// detect_version_files: Specifies which files to check for version information.
#[plugin_fn]
pub fn detect_version_files(_: ()) -> FnResult<Json<DetectVersionOutput>> {
let asdf_plugin = AsdfPlugin {
tool: Tool::default(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no default method for initializing an instance of AsdfPlugin with a default instance of Tool. Can it be implemented in Tool impl?

};
Ok(Json(asdf_plugin.detect_version_files()))
}

// parse_version_file: Parses version information from specified files
#[plugin_fn]
pub fn parse_version_file(
Json(input): Json<ParseVersionFileInput>,
) -> FnResult<Json<ParseVersionFileOutput>> {
let asdf_plugin = AsdfPlugin {
tool: Tool::default(),
};
Ok(Json(asdf_plugin.parse_version_file(input)))
}

// locate_executables: Locates the installed tool's executable files.
#[plugin_fn]
pub fn locate_executables(
Json(_): Json<LocateExecutablesInput>,
) -> FnResult<Json<LocateExecutablesOutput>> {
let env = get_host_environment()?;

Ok(Json(LocateExecutablesOutput {
primary: Some(ExecutableConfig::new(
env.os.for_native("bin/node", "node.exe"),
)),
globals_lookup_dirs: vec!["$DENO_INSTALL_ROOT/bin".into(), "$HOME/.deno/bin".into()],
..LocateExecutablesOutput::default()
}))
}
3 changes: 3 additions & 0 deletions crates/pdk-api/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ pub enum PluginError {
arch: String,
os: String,
},

#[error("Unsupported archive format: {0}")]
UnsupportedArchiveFormat(String),
}
Loading