Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/wasip3/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -195,6 +200,13 @@ pub fn run<T: Into<OsString> + Clone, I: IntoIterator<Item = T>>(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(())
}
}
}

Expand Down
134 changes: 101 additions & 33 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::{
io::Cursor,
path::{Path, PathBuf},
process::Command,
time::Duration,
};
use tar::Archive;
use wit_parser::{
Expand Down Expand Up @@ -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<PathBuf> {
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<String>,
timeout: Option<Duration>,
cache_dir: Option<PathBuf>,
) -> Result<PathBuf> {
// Determine OS and architecture
let os = match std::env::consts::OS {
"macos" => "darwin",
Expand All @@ -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");
Expand All @@ -432,23 +415,108 @@ 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<PathBuf> {
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"))?;

eprintln!("Using {}.", bin.display());

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"));
}
}
Loading