diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 3049a785ff3..5d7cd5548a3 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -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 { + 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()); diff --git a/script/download-wasi-sdk b/script/download-wasi-sdk index 888f5d91756..dad551a6fb4 100755 --- a/script/download-wasi-sdk +++ b/script/download-wasi-sdk @@ -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"