From 1b78c33543dbead91d864979db586e2e4f0ddae6 Mon Sep 17 00:00:00 2001 From: Joel Dice Date: Wed, 1 Apr 2026 12:10:08 -0600 Subject: [PATCH 1/3] add Go wrapper module This enables installing `componentize-go` using `go install` and or using it with `go tool`. When the Go binary is run for the first time, it will download, cache, and run the Rust binary. --- .gitignore | 1 + go.mod | 9 +++ go.sum | 17 +++++ main.go | 170 +++++++++++++++++++++++++++++++++++++++++++++++++ src/command.rs | 2 + 5 files changed, 199 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore index e024e0d..7a590a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +componentize-go target *.wasm diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2492461 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/bytecodealliance/componentize-go + +go 1.25 + +require github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13 + +require github.com/gofrs/flock v0.13.0 + +require golang.org/x/sys v0.37.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7fab24a --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13 h1:JtuelWqyixKApmXm3qghhZ7O96P6NKpyrlSIe8Rwnhw= +github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13/go.mod h1:7kfpUbyCdGJ9fDRCp3fopPQi5+cKNHgTE4ZuNrO71Cw= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..2ae568d --- /dev/null +++ b/main.go @@ -0,0 +1,170 @@ +package main + +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/apparentlymart/go-userdirs/userdirs" + "github.com/gofrs/flock" +) + +// This is a simple wrapper program which downloads, caches, and runs the +// appropriate `componentize-go` binary for the current platform. +// +// Although `componentize-go` is written in Rust, we can use this wrapper to +// make it available using e.g. `go install` and/or `go tool`. +func main() { + // This is hard-coded to point to the latest canary release, which is + // appropriate for the `main` branch, but should be changed to the + // appropriate release URL for each tagged release. + // + // TODO: Can we automate updating this for each release? + release := "canary" + + directories := userdirs.ForApp( + "componentize-go", + "bytecodealliance", + "com.github.bytecodealliance-componentize-go", + ) + binDirectory := filepath.Join(directories.CacheDir, "bin") + versionPath := filepath.Join(directories.CacheDir, "version.txt") + lockFilePath := filepath.Join(directories.CacheDir, "lock") + binaryPath := filepath.Join(binDirectory, "componentize-go") + + maybeDownload(release, binDirectory, versionPath, lockFilePath, binaryPath) + + run(binaryPath) +} + +// Download the specified version of `componentize-go` if we haven't already. +func maybeDownload(release, binDirectory, versionPath, lockFilePath, binaryPath string) { + if err := os.MkdirAll(binDirectory, 0755); err != nil { + log.Fatalf("unable to create directory `%v`: %v", binDirectory, err) + } + + // Lock the lock file to prevent concurrent downloads. + lockFile := flock.New(lockFilePath) + if err := lockFile.Lock(); err != nil { + log.Fatalf("unable to lock file `%v`: %v", lockFilePath, err) + } + defer lockFile.Unlock() + + versionBytes, err := os.ReadFile(versionPath) + var version string + if err != nil { + version = "" + } else { + version = strings.TrimSpace(string(versionBytes)) + } + + // If the binary doesn't already exist and/or the version doesn't match + // the desired release, download it. + if _, err := os.Stat(binaryPath); errors.Is(err, os.ErrNotExist) || version != release { + base := fmt.Sprintf( + "https://github.com/bytecodealliance/componentize-go/releases/download/%v", + release, + ) + + url := fmt.Sprintf("%v/componentize-go-%v-%v.tar.gz", base, runtime.GOOS, runtime.GOARCH) + + fmt.Printf("Downloading `componentize-go` binary from %v and extracting to %v\n", url, binDirectory) + + response, err := http.Get(url) + if err != nil { + log.Fatalf("unable to download URL `%v`: %v", url, err) + } + defer response.Body.Close() + + if response.StatusCode < 200 || response.StatusCode > 299 { + log.Fatalf("unexpected status for URL `%v`: %v", url, response.StatusCode) + } + + uncompressed, err := gzip.NewReader(response.Body) + if err != nil { + log.Fatalf("unable to decompress content of URL `%v`: %v", url, err) + } + defer uncompressed.Close() + + untarred := tar.NewReader(uncompressed) + for { + header, err := untarred.Next() + if err == io.EOF { + break + } else if err != nil { + log.Fatalf("unable to untar content of URL `%v`: %v", url, err) + } + path := filepath.Join(binDirectory, header.Name) + file, err := os.Create(path) + if err != nil { + log.Fatalf("unable to create file `%v`: %v", path, err) + } + if _, err := io.Copy(file, untarred); err != nil { + log.Fatalf("unable to untar content of URL `%v`: %v", url, err) + } + file.Close() + } + + if err := os.Chmod(binaryPath, 0755); err != nil { + log.Fatalf("unable to make file `%v` executable: %v", binaryPath, err) + } + } + + // If we just downloaded a new version, remember which one so we don't + // download it redundantly next time. + if version != release { + if err := os.WriteFile(versionPath, []byte(release), 0600); err != nil { + log.Fatalf("unable to write version to `%v`: %v", versionPath, err) + } + } +} + +// Run the specified binary, forwarding all our arguments to it and piping its +// stdout and stderr back to the user. +func run(binaryPath string) { + command := exec.Command(binaryPath, os.Args[1:]...) + + stderr, err := command.StderrPipe() + if err != nil { + log.Fatalf("unable to get stderr for `%v` command: %v", binaryPath, err) + } + + go func() { + defer stderr.Close() + io.Copy(os.Stderr, stderr) + }() + + stdout, err := command.StdoutPipe() + if err != nil { + log.Fatalf("unable to get stdout for `%v` command: %v", binaryPath, err) + } + + go func() { + defer stdout.Close() + io.Copy(os.Stdout, stdout) + }() + + if err := command.Start(); err != nil { + log.Fatalf("unable to start `%v` command: %v", binaryPath, err) + } + + if err := command.Wait(); err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + code := exiterr.ExitCode() + if code != 0 { + os.Exit(code) + } + } else { + log.Fatalf("trouble running `%v` command: %v", binaryPath, err) + } + } +} diff --git a/src/command.rs b/src/command.rs index 336e2e5..bbb36ae 100644 --- a/src/command.rs +++ b/src/command.rs @@ -50,6 +50,8 @@ pub struct WitOpts { #[arg(long, short = 'w')] pub world: Vec, + /// If `true`, skip scanning the current Go module's dependencies for + /// `componentize-go.toml` files. #[arg(long)] pub ignore_toml_files: bool, From 088ad6acc3a71f33636a5ce92f43433159ae96f5 Mon Sep 17 00:00:00 2001 From: Joel Dice Date: Mon, 6 Apr 2026 12:46:47 -0600 Subject: [PATCH 2/3] simplify stdio forwarding per review feedback --- main.go | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/main.go b/main.go index 2ae568d..a8475b1 100644 --- a/main.go +++ b/main.go @@ -132,26 +132,8 @@ func maybeDownload(release, binDirectory, versionPath, lockFilePath, binaryPath // stdout and stderr back to the user. func run(binaryPath string) { command := exec.Command(binaryPath, os.Args[1:]...) - - stderr, err := command.StderrPipe() - if err != nil { - log.Fatalf("unable to get stderr for `%v` command: %v", binaryPath, err) - } - - go func() { - defer stderr.Close() - io.Copy(os.Stderr, stderr) - }() - - stdout, err := command.StdoutPipe() - if err != nil { - log.Fatalf("unable to get stdout for `%v` command: %v", binaryPath, err) - } - - go func() { - defer stdout.Close() - io.Copy(os.Stdout, stdout) - }() + command.Stdout = os.Stdout + command.Stderr = os.Stderr if err := command.Start(); err != nil { log.Fatalf("unable to start `%v` command: %v", binaryPath, err) From 142cd25d42e83f832e77e1b038e5b34dade2b8b8 Mon Sep 17 00:00:00 2001 From: Joel Dice Date: Mon, 6 Apr 2026 12:48:24 -0600 Subject: [PATCH 3/3] tweak comment for clarity --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index a8475b1..03542f5 100644 --- a/main.go +++ b/main.go @@ -25,8 +25,8 @@ import ( // make it available using e.g. `go install` and/or `go tool`. func main() { // This is hard-coded to point to the latest canary release, which is - // appropriate for the `main` branch, but should be changed to the - // appropriate release URL for each tagged release. + // appropriate for the `main` branch, but should be changed to the tag + // name for each tagged release. // // TODO: Can we automate updating this for each release? release := "canary"