extension: Bump wasi-sdk to version 34 (#63816)

# Objective

Bumps `wasi-sdk` to v34 that is based on LLVM 23.1 which includes [a
compile time and memory usage
fix](https://github.com/llvm/llvm-project/pull/181755) for tree-sitter
grammars. It speeds up every grammar's compile time and reduces compile
memory usage unlocking big grammars that were impossible to compile
before to be submitted to Zed extensions.

Drive-by added additional wasi-sdk targets for the rest of the supported
architectures (by wasi-sdk), if riscv is unwanted I can remove it but it
should not create any problems (I think).

Also noticed the `download-wasi-sdk` script had `OS=mingw` which wasn't
even in the artifact name on v25.

## Testing

Compiled, ran and compiled my [big
grammar](https://github.com/usagi-coffee/tree-sitter-abl) on it, that
wouldn't compile at all pre-llvm 23.

## Self-Review Checklist:

- [X] I've reviewed my own diff for quality, security, and reliability

---

Release Notes:

- Fixed compiling dev extensions with big Tree-sitter grammars.
- Fixed compiling dev extensions on Windows ARM64.
- Added wasi-sdk installation invalidation.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
Kamil Jakubus 2026-09-09 15:16:15 +00:00 • committed by GitHub
parent e9d2934edf
commit a3e93fffaa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 261 additions and 226 deletions

View file

@ -2,7 +2,7 @@ use crate::{
ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, parse_wasm_extension_version,
};
use ::fs::Fs;
use anyhow::{Context as _, Result, bail};
use anyhow::{Context as _, Result};
use futures::{
FutureExt, StreamExt,
channel::oneshot::{self, Sender},
@ -28,20 +28,19 @@ use wasmparser::Parser;
/// Currently, we compile with Rust's `wasm32-wasip2` target, which works with WASI `preview2` and the component model.
const RUST_TARGET: &str = "wasm32-wasip2";
/// Compiling Tree-sitter parsers from C to WASM requires Clang 17, and a WASM build of libc
/// and clang's runtime library. The `wasi-sdk` provides these binaries.
///
/// Once Clang 17 and its wasm target are available via system package managers, we won't need
/// to download this.
const WASI_SDK_URL: &str = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/";
const WASI_SDK_ASSET_NAME: Option<&str> = cfg_select! {
all(target_os = "macos", target_arch = "x86_64") => Some("wasi-sdk-25.0-x86_64-macos.tar.gz"),
all(target_os = "macos", target_arch = "aarch64") => Some("wasi-sdk-25.0-arm64-macos.tar.gz"),
all(target_os = "linux", target_arch = "x86_64") => Some("wasi-sdk-25.0-x86_64-linux.tar.gz"),
all(target_os = "linux", target_arch = "aarch64") => Some("wasi-sdk-25.0-arm64-linux.tar.gz"),
all(target_os = "freebsd", target_arch = "x86_64") => Some("wasi-sdk-25.0-x86_64-linux.tar.gz"),
all(target_os = "freebsd", target_arch = "aarch64") => Some("wasi-sdk-25.0-arm64-linux.tar.gz"),
all(target_os = "windows", target_arch = "x86_64") => Some("wasi-sdk-25.0-x86_64-windows.tar.gz"),
/// Compiling Tree-sitter parsers from C to WebAssembly requires a compiler,
/// a WASI sysroot, and compiler runtime libraries, provided by `wasi-sdk`.
const WASI_SDK_VERSION: &str = "34";
const WASI_SDK_URL: &str = "https://github.com/WebAssembly/wasi-sdk/releases/download/";
const WASI_SDK_PLATFORM: Option<&str> = cfg_select! {
all(target_os = "macos", target_arch = "x86_64") => Some("x86_64-macos"),
all(target_os = "macos", target_arch = "aarch64") => Some("arm64-macos"),
all(target_os = "linux", target_arch = "x86_64") => Some("x86_64-linux"),
all(target_os = "linux", target_arch = "aarch64") => Some("arm64-linux"),
all(target_os = "freebsd", target_arch = "x86_64") => Some("x86_64-linux"),
all(target_os = "freebsd", target_arch = "aarch64") => Some("arm64-linux"),
all(target_os = "windows", target_arch = "x86_64") => Some("x86_64-windows"),
all(target_os = "windows", target_arch = "aarch64") => Some("arm64-windows"),
_ => None
};
@ -100,16 +99,14 @@ impl ExtensionBuilder {
populate_defaults(extension_manifest, extension_dir, fs.clone()).await?;
if extension_dir.is_relative() {
bail!(
"extension dir {} is not an absolute path",
extension_dir.display()
);
}
anyhow::ensure!(
extension_dir.is_absolute(),
"extension dir {extension_dir:?} is not an absolute path"
);
fs.create_dir(&self.cache_dir)
.await
.context("failed to create cache dir")?;
.context("creating cache directory")?;
let (tx, mut rx) = oneshot::channel();
@ -129,12 +126,12 @@ impl ExtensionBuilder {
let rust_compilation_task =
(extension_manifest.lib.kind == Some(ExtensionLibraryKind::Rust)).then(|| {
async {
log::info!("compiling Rust extension {}", extension_dir.display());
log::info!("compiling Rust extension {extension_dir:?}");
self.compile_rust_extension(extension_dir, extension_manifest, tx, &options)
.await
.context("failed to compile Rust extension")?;
.context("compiling Rust extension")?;
log::info!("compiled Rust extension {}", extension_dir.display());
log::info!("compiled Rust extension {extension_dir:?}");
Ok(())
}
.boxed()
@ -147,21 +144,15 @@ impl ExtensionBuilder {
.map(|((grammar_name, grammar_metadata), clang_path_task)| {
async move {
let snake_cased_grammar_name = grammar_name.to_snake_case();
if grammar_name.as_ref() != snake_cased_grammar_name.as_str() {
bail!(
"grammar name '{grammar_name}' must be \
written in snake_case: {snake_cased_grammar_name}"
);
}
log::info!(
"compiling grammar {grammar_name} for extension {}",
extension_dir.display()
anyhow::ensure!(
grammar_name.as_ref() == snake_cased_grammar_name.as_str(),
"grammar name '{grammar_name}' must be \
written in snake_case: {snake_cased_grammar_name}"
);
let clang_path = clang_path_task
.await
.context("Failed to resolve clang path")?;
log::info!("compiling grammar {grammar_name} for extension {extension_dir:?}");
let clang_path = clang_path_task.await.context("resolving clang path")?;
self.compile_grammar(
extension_dir,
@ -170,11 +161,8 @@ impl ExtensionBuilder {
&clang_path,
)
.await
.with_context(|| format!("failed to compile grammar '{grammar_name}'"))?;
log::info!(
"compiled grammar {grammar_name} for extension {}",
extension_dir.display()
);
.with_context(|| format!("compiling grammar '{grammar_name}'"))?;
log::info!("compiled grammar {grammar_name} for extension {extension_dir:?}");
Ok(())
}
@ -204,8 +192,7 @@ impl ExtensionBuilder {
}
log::info!(
"finished compiling extension {} in {time:.2}s",
extension_dir.display(),
"finished compiling extension {extension_dir:?} in {time:.2}s",
time = start.elapsed().as_secs_f64(),
);
Ok(())
@ -223,10 +210,7 @@ impl ExtensionBuilder {
let cargo_toml_content = fs::read_to_string(extension_dir.join("Cargo.toml"))?;
let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content)?;
log::info!(
"compiling Rust crate for extension {}",
extension_dir.display()
);
log::info!("compiling Rust crate for extension {extension_dir:?}");
let output = util::command::new_command("cargo")
.args(["build", "--target", RUST_TARGET])
.args(options.release.then_some("--release"))
@ -237,19 +221,15 @@ impl ExtensionBuilder {
.current_dir(extension_dir)
.output()
.await
.context("failed to run `cargo`")?;
if !output.status.success() {
bail!(
"failed to build extension {}",
String::from_utf8_lossy(&output.stderr)
);
}
log::info!(
"compiled Rust crate for extension {}",
extension_dir.display()
.context("running `cargo`")?;
anyhow::ensure!(
output.status.success(),
"failed to build extension {}",
String::from_utf8_lossy(&output.stderr)
);
log::info!("compiled Rust crate for extension {extension_dir:?}");
let mut wasm_path = PathBuf::from(extension_dir);
wasm_path.extend([
"target",
@ -263,17 +243,14 @@ impl ExtensionBuilder {
]);
wasm_path.set_extension("wasm");
log::info!(
"encoding wasm component for extension {}",
extension_dir.display()
);
log::info!("encoding wasm component for extension {extension_dir:?}");
let component_bytes = fs::read(&wasm_path)
.with_context(|| format!("failed to read output module `{}`", wasm_path.display()))?;
let component_bytes =
fs::read(&wasm_path).with_context(|| format!("reading output module {wasm_path:?}"))?;
let component_bytes = self
.strip_custom_sections(&component_bytes)
.context("failed to strip debug sections from wasm component")?;
.context("stripping debug sections from wasm component")?;
let wasm_extension_api_version =
parse_wasm_extension_version(&manifest.id, &component_bytes)
@ -283,14 +260,9 @@ impl ExtensionBuilder {
.map_err(|_| anyhow::anyhow!("Failed to send API version"))?;
let extension_file = extension_dir.join("extension.wasm");
fs::write(extension_file.clone(), &component_bytes)
.context("failed to write extension.wasm")?;
fs::write(extension_file.clone(), &component_bytes).context("writing extension.wasm")?;
log::info!(
"extension {} written to {}",
extension_dir.display(),
extension_file.display()
);
log::info!("extension {extension_dir:?} written to {extension_file:?}");
Ok(())
}
@ -345,15 +317,13 @@ impl ExtensionBuilder {
.args(scanner_path.exists().then_some(scanner_path))
.output()
.await
.context("failed to run clang")?;
.context("running clang")?;
if !clang_output.status.success() {
bail!(
"failed to compile {} parser with clang: {}",
grammar_name,
String::from_utf8_lossy(&clang_output.stderr),
);
}
anyhow::ensure!(
clang_output.status.success(),
"failed to compile {grammar_name} parser with clang: {}",
String::from_utf8_lossy(&clang_output.stderr),
);
}
Ok(())
@ -372,28 +342,22 @@ impl ExtensionBuilder {
.await?;
let has_remote = remotes_output.status.success()
&& String::from_utf8_lossy(&remotes_output.stdout).trim() == url;
if !has_remote {
bail!(
"grammar directory '{}' already exists, but is not a git clone of '{}'",
directory.display(),
url
);
}
anyhow::ensure!(
has_remote,
"grammar directory {directory:?} already exists, but is not a git clone of '{url}'"
);
} else {
fs::create_dir_all(directory).with_context(|| {
format!("failed to create grammar directory {}", directory.display(),)
})?;
fs::create_dir_all(directory)
.with_context(|| format!("creating grammar directory {directory:?}"))?;
let init_output = util::command::new_command("git")
.arg("init")
.current_dir(directory)
.output()
.await?;
if !init_output.status.success() {
bail!(
"failed to run `git init` in directory '{}'",
directory.display()
);
}
anyhow::ensure!(
init_output.status.success(),
"failed to run `git init` in directory {directory:?}"
);
let remote_add_output = util::command::new_command("git")
.arg("--git-dir")
@ -401,13 +365,11 @@ impl ExtensionBuilder {
.args(["remote", "add", "origin", url])
.output()
.await
.context("failed to execute `git remote add`")?;
if !remote_add_output.status.success() {
bail!(
"failed to add remote {url} for git repository {}",
git_dir.display()
);
}
.context("executing `git remote add`")?;
anyhow::ensure!(
remote_add_output.status.success(),
"failed to add remote {url} for git repository {git_dir:?}"
);
}
let fetch_output = util::command::new_command("git")
@ -416,7 +378,7 @@ impl ExtensionBuilder {
.args(["fetch", "--depth", "1", "origin", rev])
.output()
.await
.context("failed to execute `git fetch`")?;
.context("executing `git fetch`")?;
let checkout_output = util::command::new_command("git")
.arg("--git-dir")
@ -425,19 +387,14 @@ impl ExtensionBuilder {
.current_dir(directory)
.output()
.await
.context("failed to execute `git checkout`")?;
.context("executing `git checkout`")?;
if !checkout_output.status.success() {
if !fetch_output.status.success() {
bail!(
"failed to fetch revision {} in directory '{}'",
rev,
directory.display()
);
}
bail!(
"failed to checkout revision {} in directory '{}': {}",
rev,
directory.display(),
anyhow::ensure!(
fetch_output.status.success(),
"failed to fetch revision {rev} in directory {directory:?}"
);
anyhow::bail!(
"failed to checkout revision {rev} in directory {directory:?}: {}",
String::from_utf8_lossy(&checkout_output.stderr)
);
}
@ -450,27 +407,25 @@ impl ExtensionBuilder {
.args(["--print", "target-libdir", "--target", RUST_TARGET])
.output()
.await
.context("failed to run rustc")?;
if !rustc_output.status.success() {
bail!(
"failed to retrieve the `{RUST_TARGET}` target libdir: {}",
String::from_utf8_lossy(&rustc_output.stderr)
);
}
.context("running rustc")?;
anyhow::ensure!(
rustc_output.status.success(),
"failed to retrieve the `{RUST_TARGET}` target libdir: {}",
String::from_utf8_lossy(&rustc_output.stderr)
);
let target_libdir = PathBuf::from(String::from_utf8(rustc_output.stdout)?.trim());
if target_libdir.exists() {
return Ok(());
}
if which::which("rustup").is_err() {
bail!(
"the `{RUST_TARGET}` target is not installed, and `rustup` is not available to \
install it. Add the target to your Rust toolchain (e.g. `targets = \
[\"{RUST_TARGET}\"]` for Nix rust-overlay/fenix toolchains) or install it via \
your package manager"
);
}
anyhow::ensure!(
which::which("rustup").is_ok(),
"the `{RUST_TARGET}` target is not installed, and `rustup` is not available to \
install it. Add the target to your Rust toolchain (e.g. `targets = \
[\"{RUST_TARGET}\"]` for Nix rust-overlay/fenix toolchains) or install it via \
your package manager"
);
let output = util::command::new_command("rustup")
.args(["target", "add", RUST_TARGET])
@ -478,13 +433,12 @@ impl ExtensionBuilder {
.stdout(Stdio::inherit())
.output()
.await
.context("failed to run `rustup target add`")?;
if !output.status.success() {
bail!(
"failed to install the `{RUST_TARGET}` target: {}",
String::from_utf8_lossy(&output.stderr)
);
}
.context("running `rustup target add`")?;
anyhow::ensure!(
output.status.success(),
"failed to install the `{RUST_TARGET}` target: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
@ -502,42 +456,69 @@ impl ExtensionBuilder {
);
}
let url = if let Some(asset_name) = WASI_SDK_ASSET_NAME {
format!("{WASI_SDK_URL}{asset_name}")
} else {
bail!("wasi-sdk is not available for platform {}", env::consts::OS);
};
let wasi_sdk_dir = self.cache_dir.join("wasi-sdk");
let clang_path = wasi_sdk_clang_path(&wasi_sdk_dir);
log::info!("downloading wasi-sdk to {}", wasi_sdk_dir.display());
if fs::metadata(&clang_path).is_ok_and(|metadata| metadata.is_file()) {
return Ok(clang_path);
match self
.update_wasi_sdk_if_needed(&wasi_sdk_dir, &clang_path)
.await
{
Ok(()) => Ok(clang_path),
Err(error) => {
if fs::metadata(&clang_path).is_ok_and(|metadata| metadata.is_file()) {
let installed_version = installed_wasi_sdk_version(&wasi_sdk_dir)
.unwrap_or_else(|| "unknown".to_string());
log::warn!(
"failed to install wasi-sdk {WASI_SDK_VERSION}.0, using cached wasi-sdk {installed_version} from {wasi_sdk_dir:?}: {error:#}"
);
Ok(clang_path)
} else {
Err(error)
}
}
}
}
async fn update_wasi_sdk_if_needed(
&self,
wasi_sdk_dir: &Path,
clang_path: &Path,
) -> Result<()> {
if installed_wasi_sdk_version(wasi_sdk_dir) == Some(format!("{WASI_SDK_VERSION}.0"))
&& fs::metadata(clang_path).is_ok_and(|metadata| metadata.is_file())
{
return Ok(());
}
let platform = WASI_SDK_PLATFORM.with_context(|| {
format!("wasi-sdk is not available for platform {}", env::consts::OS)
})?;
let url = format!(
"{WASI_SDK_URL}wasi-sdk-{WASI_SDK_VERSION}/wasi-sdk-{WASI_SDK_VERSION}.0-{platform}.tar.gz"
);
log::info!("downloading wasi-sdk to {wasi_sdk_dir:?}");
let tar_out_dir = self.cache_dir.join("wasi-sdk-temp");
fs::remove_dir_all(&wasi_sdk_dir).ok();
fs::remove_dir_all(&tar_out_dir).ok();
fs::create_dir_all(&tar_out_dir).context("failed to create extraction directory")?;
fs::create_dir_all(&tar_out_dir).context("creating extraction directory")?;
let mut response = self.http.get(&url, AsyncBody::default(), true).await?;
// Write the response to a temporary file
let tar_gz_path = self.cache_dir.join("wasi-sdk.tar.gz");
let tar_gz_file =
fs::File::create(&tar_gz_path).context("failed to create temporary tar.gz file")?;
fs::File::create(&tar_gz_path).context("creating temporary tar.gz file")?;
let response_body = response.body_mut();
let mut async_file = io::AllowStdIo::new(tar_gz_file);
io::copy(response_body, &mut async_file)
.await
.context("failed to stream response to file")?;
.context("streaming response to file")?;
drop(async_file);
log::info!("un-tarring wasi-sdk to {}", tar_out_dir.display());
log::info!("un-tarring wasi-sdk to {tar_out_dir:?}");
// Shell out to tar to extract the archive
let tar_output = util::command::new_command("tar")
@ -547,14 +528,13 @@ impl ExtensionBuilder {
.arg(&tar_out_dir)
.output()
.await
.context("failed to run tar")?;
.context("running tar")?;
if !tar_output.status.success() {
bail!(
"failed to extract wasi-sdk archive: {}",
String::from_utf8_lossy(&tar_output.stderr)
);
}
anyhow::ensure!(
tar_output.status.success(),
"failed to extract wasi-sdk archive: {}",
String::from_utf8_lossy(&tar_output.stderr)
);
log::info!("finished downloading wasi-sdk");
@ -564,12 +544,17 @@ impl ExtensionBuilder {
let inner_dir = fs::read_dir(&tar_out_dir)?
.next()
.context("no content")?
.context("failed to read contents of extracted wasi archive directory")?
.context("reading contents of extracted wasi archive directory")?
.path();
fs::rename(&inner_dir, &wasi_sdk_dir).context("failed to move extracted wasi dir")?;
match fs::remove_dir_all(wasi_sdk_dir) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error).context("removing outdated wasi-sdk"),
}
fs::rename(&inner_dir, wasi_sdk_dir).context("moving extracted wasi directory")?;
fs::remove_dir_all(&tar_out_dir).ok();
Ok(clang_path)
Ok(())
}
// This was adapted from:
@ -665,7 +650,7 @@ async fn populate_defaults(
let mut language_dir_entries = fs
.read_dir(&languages_dir)
.await
.context("failed to list languages dir")?;
.context("listing languages directory")?;
while let Some(language_dir) = language_dir_entries.next().await {
let language_dir = language_dir?;
@ -686,7 +671,7 @@ async fn populate_defaults(
let mut theme_dir_entries = fs
.read_dir(&themes_dir)
.await
.context("failed to list themes dir")?;
.context("listing themes directory")?;
while let Some(theme_path) = theme_dir_entries.next().await {
let theme_path = theme_path?;
@ -705,7 +690,7 @@ async fn populate_defaults(
let mut icon_theme_dir_entries = fs
.read_dir(&icon_themes_dir)
.await
.context("failed to list icon themes dir")?;
.context("listing icon themes directory")?;
while let Some(icon_theme_path) = icon_theme_dir_entries.next().await {
let icon_theme_path = icon_theme_path?;
@ -734,7 +719,7 @@ async fn populate_defaults(
let mut grammar_dir_entries = fs
.read_dir(&grammars_dir)
.await
.context("failed to list grammars dir")?;
.context("listing grammars directory")?;
while let Some(grammar_path) = grammar_dir_entries.next().await {
let grammar_path = grammar_path?;
@ -801,6 +786,20 @@ fn wasi_sdk_clang_path(wasi_sdk_dir: &Path) -> PathBuf {
clang_path
}
fn installed_wasi_sdk_version(wasi_sdk_dir: &Path) -> Option<String> {
let version_path = wasi_sdk_dir.join("VERSION");
let contents = match fs::read_to_string(&version_path) {
Ok(contents) => contents,
Err(error) => {
if error.kind() != std::io::ErrorKind::NotFound {
log::warn!("failed to read wasi-sdk version from {version_path:?}: {error}");
}
return None;
}
};
contents.lines().next().map(str::to_string)
}
#[cfg(test)]
mod tests {
use std::{
@ -815,7 +814,7 @@ mod tests {
use crate::{
ExtensionManifest, ExtensionSnippets,
extension_builder::{file_newer_than_deps, populate_defaults},
extension_builder::{file_newer_than_deps, installed_wasi_sdk_version, populate_defaults},
};
#[test]
@ -856,6 +855,29 @@ mod tests {
);
}
#[test]
fn test_installed_wasi_sdk_version() {
let tmpdir = tempfile::tempdir().unwrap();
let version_path = tmpdir.path().join("VERSION");
assert_eq!(installed_wasi_sdk_version(tmpdir.path()), None);
std::fs::write(&version_path, "").unwrap();
assert_eq!(installed_wasi_sdk_version(tmpdir.path()), None);
std::fs::write(&version_path, "25.0\nwasi-libc: 574b88da4815\n").unwrap();
assert_eq!(
installed_wasi_sdk_version(tmpdir.path()),
Some("25.0".to_string())
);
std::fs::write(&version_path, "34.0\r\nwasi-libc: 2e6fb9d8ee0c\r\n").unwrap();
assert_eq!(
installed_wasi_sdk_version(tmpdir.path()),
Some("34.0".to_string())
);
}
#[gpui::test]
async fn test_snippet_location_is_kept(cx: &mut TestAppContext) {
let fs = fs::FakeFs::new(cx.executor());

View file

@ -1,61 +1,74 @@
#!/bin/bash
set -euo pipefail
# Check if ./target/wasi-sdk exists
if [ ! -d "./target/wasi-sdk" ]; then
echo "WASI SDK not found, downloading v25..."
WASI_SDK_VERSION="34"
WASI_SDK_DIR="./target/wasi-sdk"
# Determine OS and architecture
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
# Map architecture names to WASI SDK format
case $ARCH in
x86_64)
ARCH="x86_64"
;;
arm64|aarch64)
ARCH="arm64"
;;
*)
echo "Unsupported architecture: $ARCH"
exit 1
;;
esac
# Map OS names to WASI SDK format
case $OS in
darwin)
OS="macos"
;;
linux)
OS="linux"
;;
mingw*|msys*|cygwin*)
OS="mingw"
;;
*)
echo "Unsupported OS: $OS"
exit 1
;;
esac
# Construct download URL
WASI_SDK_VERSION="25"
WASI_SDK_URL="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-${ARCH}-${OS}.tar.gz"
echo "Downloading from: $WASI_SDK_URL"
# Create target directory if it doesn't exist
mkdir -p ./target
# Download and extract
curl -fL "$WASI_SDK_URL" | tar -xz -C ./target
# Rename the extracted directory to wasi-sdk
mv "./target/wasi-sdk-${WASI_SDK_VERSION}.0-${ARCH}-${OS}" "./target/wasi-sdk"
echo "WASI SDK v25 installed successfully"
else
echo "WASI SDK already exists at ./target/wasi-sdk"
installed_version=""
if [ -f "${WASI_SDK_DIR}/VERSION" ]; then
installed_version=$(head -n 1 "${WASI_SDK_DIR}/VERSION" | tr -d '\r')
fi
if [ "${installed_version}" = "${WASI_SDK_VERSION}.0" ]; then
echo "WASI SDK v${WASI_SDK_VERSION} already exists at ${WASI_SDK_DIR}"
exit 0
fi
if [ -d "${WASI_SDK_DIR}" ]; then
echo "WASI SDK at ${WASI_SDK_DIR} is version '${installed_version}', replacing with v${WASI_SDK_VERSION}..."
else
echo "WASI SDK not found, downloading v${WASI_SDK_VERSION}..."
fi
# Determine OS and architecture
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
# Map architecture names to WASI SDK format
case $ARCH in
x86_64)
ARCH="x86_64"
;;
arm64|aarch64)
ARCH="arm64"
;;
*)
echo "Unsupported architecture: $ARCH"
exit 1
;;
esac
# Map OS names to WASI SDK format
case $OS in
darwin)
OS="macos"
;;
linux)
OS="linux"
;;
mingw*|msys*|cygwin*)
OS="windows"
;;
*)
echo "Unsupported OS: $OS"
exit 1
;;
esac
# Construct download URL
WASI_SDK_URL="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-${ARCH}-${OS}.tar.gz"
echo "Downloading from: $WASI_SDK_URL"
# Create target directory if it doesn't exist
mkdir -p ./target
# Download and extract
rm -rf "./target/wasi-sdk-${WASI_SDK_VERSION}.0-${ARCH}-${OS}"
curl -fL "$WASI_SDK_URL" | tar -xz -C ./target
# Rename the extracted directory to wasi-sdk
rm -rf "${WASI_SDK_DIR}"
mv "./target/wasi-sdk-${WASI_SDK_VERSION}.0-${ARCH}-${OS}" "${WASI_SDK_DIR}"
echo "WASI SDK v${WASI_SDK_VERSION} installed successfully"