diff --git a/examples/wasip3/Makefile b/examples/wasip3/Makefile index a1c3a28..4c7aea4 100644 --- a/examples/wasip3/Makefile +++ b/examples/wasip3/Makefile @@ -3,7 +3,7 @@ generate-bindings: go mod tidy build-component: generate-bindings - componentize-go --world wasip3-example build --go ../../$(GO)/bin/go + componentize-go --world wasip3-example build .PHONY: run run: build-component diff --git a/src/command.rs b/src/command.rs index bbb36ae..7dee8a8 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2,7 +2,7 @@ use crate::{ cmd_bindings::generate_bindings, cmd_build::build_module, cmd_test::build_test_module, - utils::{dummy_wit, embed_wit, module_to_component, parse_wit, pick_go}, + utils::{dummy_wit, embed_wit, install_go, module_to_component, parse_wit, pick_go}, }; use anyhow::{Result, anyhow}; use clap::{Parser, Subcommand}; @@ -79,6 +79,11 @@ pub enum Command { /// Generate Go bindings for a WIT world. Bindings(Bindings), + + /// Install a patched version of Go that's compatible with component model async. + /// + /// WARNING: this is a temporary workaround, and will be removed once https://github.com/golang/go/pull/76775 is resolved + InstallGo, } #[derive(Parser)] @@ -195,6 +200,13 @@ pub fn run + Clone, I: IntoIterator>(args: I) -> Res Command::Build(opts) => build(options.wit_opts, opts), Command::Bindings(opts) => bindings(options.wit_opts, opts), Command::Test(opts) => test(options.wit_opts, opts), + Command::InstallGo => { + eprintln!( + "warning: `install-go` is a temporary workaround, and will be removed once https://github.com/golang/go/pull/76775 is resolved" + ); + let _ = install_go(None, None, None)?; + Ok(()) + } } } diff --git a/src/utils.rs b/src/utils.rs index 2405e9f..ec1d5d4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,6 +7,7 @@ use std::{ io::Cursor, path::{Path, PathBuf}, process::Command, + time::Duration, }; use tar::Archive; use wit_parser::{ @@ -376,35 +377,11 @@ fn world_needs_async(resolve: &Resolve, world: WorldId) -> bool { }) } -pub fn pick_go(resolve: &Resolve, world: WorldId, go_path: Option<&Path>) -> Result { - let go = match go_path { - Some(p) => Some(make_path_absolute(p)?), - None => which::which("go").ok(), - }; - - if let Some(go) = go { - if world_needs_async(resolve, world) && check_go_async_support(&go).is_none() { - eprintln!( - "Note: {} does not support async operation; will use downloaded version.\n\ - See https://github.com/golang/go/pull/76775 for details.", - go.display() - ) - } else if check_go_version(&go).is_err() { - eprintln!( - "Note: {} is not a compatible version of Go; will use downloaded version.", - go.display() - ); - } else { - return Ok(go); - } - } else { - eprintln!("Note: `go` command not found; will use downloaded version."); - } - - let Some(cache_dir) = dirs::cache_dir() else { - bail!("unable to determine cache directory for current user"); - }; - +pub fn install_go( + url: Option, + timeout: Option, + cache_dir: Option, +) -> Result { // Determine OS and architecture let os = match std::env::consts::OS { "macos" => "darwin", @@ -420,7 +397,13 @@ pub fn pick_go(resolve: &Resolve, world: WorldId, go_path: Option<&Path>) -> Res bad_arch => panic!("ARCH not supported: {bad_arch}"), }; - let cache_dir = &cache_dir.join("componentize-go").join("v2"); + let cache_dir = &cache_dir.unwrap_or({ + let Some(cache_dir) = dirs::cache_dir() else { + bail!("unable to determine cache directory for current user"); + }; + cache_dir.join("componentize-go").join("v2") + }); + let name = &format!("go-{os}-{arch}-bootstrap"); let dir = cache_dir.join(name); let bin = dir.join("bin").join("go"); @@ -432,19 +415,65 @@ pub fn pick_go(resolve: &Resolve, world: WorldId, go_path: Option<&Path>) -> Res lock_file.lock()?; if !bin.exists() { - let url = format!( + let url = url.unwrap_or(format!( "https://github.com/dicej/go/releases/download/go1.25.5-wasi-on-idle-v2/{name}.tbz" - ); + )); + + // Provide a generous timeout window for users with slow internet + let client = reqwest::blocking::Client::builder() + .connect_timeout(Duration::from_secs(15)) + .timeout(timeout.unwrap_or(Duration::from_mins(10))) + .build()?; eprintln!("Downloading patched Go from {url}."); - let content = reqwest::blocking::get(&url)?.error_for_status()?.bytes()?; + let content = (|| -> Result<_> { + client + .get(&url) + .send() + .with_context(|| format!("failed to connect to {url}"))? + .error_for_status() + .with_context(|| format!("server returned an error status for {url}"))? + .bytes() + .with_context(|| format!("download of {url} was interrupted")) + })() + .context("failed to install patched version of Go\n\nRun `componentize-go install-go` to try again")?; eprintln!("Extracting patched Go to {}.", cache_dir.display()); Archive::new(BzDecoder::new(Cursor::new(content))).unpack(cache_dir)?; } + let bin: PathBuf = dir.join("bin").join("go"); + Ok(bin) +} + +pub fn pick_go(resolve: &Resolve, world: WorldId, go_path: Option<&Path>) -> Result { + let go = match go_path { + Some(p) => Some(make_path_absolute(p)?), + None => which::which("go").ok(), + }; + + if let Some(go) = go { + if world_needs_async(resolve, world) && check_go_async_support(&go).is_none() { + eprintln!( + "Note: {} does not support async operation; will use downloaded version.\n\ + See https://github.com/golang/go/pull/76775 for details.", + go.display() + ) + } else if check_go_version(&go).is_err() { + eprintln!( + "Note: {} is not a compatible version of Go; will use downloaded version.", + go.display() + ); + } else { + return Ok(go); + } + } else { + eprintln!("Note: `go` command not found; will use downloaded version."); + } + + let bin = install_go(None, None, None)?; check_go_version(&bin)?; check_go_async_support(&bin).ok_or_else(|| anyhow!("downloaded Go does not support async"))?; @@ -452,3 +481,42 @@ pub fn pick_go(resolve: &Resolve, world: WorldId, go_path: Option<&Path>) -> Res Ok(bin) } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Read; + use std::net::TcpListener; + use std::thread; + use std::time::Duration; + + #[test] + fn test_install_go_times_out_on_stalled_server() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + + // Accept the connection but never write a response + thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut buf = [0u8; 1024]; + // Read the request, then idle for 60s + let _ = stream.read(&mut buf); + thread::sleep(Duration::from_secs(60)); + }); + + let url = format!("http://{addr}/go.tbz"); + let result = install_go( + Some(url), + Some(Duration::from_secs(1)), + Some(std::env::temp_dir()), + ); + assert!(result.is_err()); + + let err_str = format!("{result:?}"); + + // Make sure it's actually a timeout error + assert!(err_str.contains("operation timed out")); + // Make sure the instruction redirecting the user is present + assert!(err_str.contains("Run `componentize-go install-go` to try again")); + } +}