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
23 changes: 23 additions & 0 deletions tools/loom_wrapper/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Cross-platform wrapper for the LOOM WASM optimizer component.

This wrapper executes the pre-built loom.wasm component via wasmtime,
resolving symlinks to real paths and preopening the resolved directories so
LOOM can read fetched/adopted component inputs that Bazel stages as symlinks
(issue #490). Mirrors //tools/wasmsign2_wrapper.
"""

load("@rules_go//go:def.bzl", "go_binary")

package(default_visibility = ["//visibility:public"])

# Wrapper binary that executes loom.wasm with symlink/path resolution.
# Both the wasmtime binary and loom.wasm are supplied by the calling rule as
# arguments (and staged as action inputs); wasmtime opens both natively. They
# are NOT resolved via runfiles, which would embed a canonical repo name that
# breaks when rules_wasm_component is consumed as a dependency (issue #490).
go_binary(
name = "loom_wrapper",
srcs = ["main.go"],
pure = "on", # Pure Go for cross-platform compatibility
visibility = ["//visibility:public"],
)
155 changes: 155 additions & 0 deletions tools/loom_wrapper/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package main

import (
"log"
"os"
"os/exec"
"path/filepath"
)

// loom_wrapper runs the loom.wasm optimizer component under wasmtime.
//
// It resolves symlinked file paths in the loom command to their real
// locations and preopens those real directories for wasmtime. Bazel stages
// a fetched/adopted input (e.g. an http_file-sourced component) as a symlink
// whose target escapes the exec root; wasmtime's WASI sandbox (cap-std)
// refuses to follow such a symlink under a plain `--dir=.` preopen, so loom
// reports "Input file not found" (issue #490). Resolving the symlink on the
// host and preopening the resolved directory gives loom a real directory to
// read from.
//
// Usage:
//
// loom_wrapper <wasmtime> <loom.wasm> <command> [args...]
//
// e.g. loom_wrapper .../wasmtime .../loom.wasm optimize <input.wasm> -o <output.wasm> [flags]
//
// Both the wasmtime binary and the loom.wasm module path are passed by the
// Bazel rule (and staged as action inputs). They are deliberately NOT located
// via the wrapper's runfiles: a hardcoded runfiles Rlocation embeds the
// canonical repo name, which differs when rules_wasm_component is the root
// module vs a dependency (the latter gains a `rules_wasm_component+` prefix),
// breaking downstream consumers (issue #490 follow-up). wasmtime opens both
// files natively, so neither needs a WASI mount.
func main() {
if len(os.Args) < 4 {
log.Fatal("Usage: loom_wrapper <wasmtime> <loom.wasm> <command> [args...]")
}

wasmtimeBinary := os.Args[1]
loomWasm := os.Args[2]
loomArgs := os.Args[3:]

if _, err := os.Stat(wasmtimeBinary); err != nil {
log.Fatalf("Wasmtime binary not found at %s: %v", wasmtimeBinary, err)
}

// Resolve file-path arguments to real paths and collect the directories
// that must be preopened for loom's WASI sandbox.
resolvedArgs, dirs := resolvePathsInArgs(loomArgs)

// Build the wasmtime command: run [--dir <real dir>...] <loom.wasm> <args>.
wasmtimeArgs := []string{"run"}
for _, dir := range uniqueStrings(dirs) {
wasmtimeArgs = append(wasmtimeArgs, "--dir", dir)
}
wasmtimeArgs = append(wasmtimeArgs, loomWasm)
wasmtimeArgs = append(wasmtimeArgs, resolvedArgs...)

cmd := exec.Command(wasmtimeBinary, wasmtimeArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin

if err := cmd.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
os.Exit(exitErr.ExitCode())
}
log.Fatalf("Failed to execute wasmtime: %v", err)
}
}

// resolvePathsInArgs resolves file-path arguments in a loom command to their
// real on-disk paths and returns the resolved argument list together with the
// directories that must be preopened (`--dir`) for wasmtime.
//
// Two argument shapes carry paths in loom's optimize command:
// - the value following a path flag (-o/--output, -i/--input), and
// - the positional input file (e.g. `optimize <input.wasm>`).
//
// Flag values that are not paths (e.g. `--attestation false`,
// `--passes cse,inline`) and the subcommand token itself are left untouched:
// they are not existing files, so the positional-path heuristic skips them.
func resolvePathsInArgs(args []string) ([]string, []string) {
resolvedArgs := make([]string, 0, len(args))
dirs := make([]string, 0)

pathFlags := map[string]bool{
"-o": true, "--output": true,
"-i": true, "--input": true,
}

for i := 0; i < len(args); i++ {
arg := args[i]

switch {
case pathFlags[arg] && i+1 < len(args):
// "<flag> <path>" form. The output path does not exist yet, so
// resolve falls back to an absolute path; its parent directory
// (under bazel-out) does exist and is preopened.
resolvedArgs = append(resolvedArgs, arg)
i++
real := resolvePath(args[i])
resolvedArgs = append(resolvedArgs, real)
dirs = append(dirs, filepath.Dir(real))

case isExistingFile(arg):
// Positional input file (e.g. the component to optimize).
real := resolvePath(arg)
resolvedArgs = append(resolvedArgs, real)
dirs = append(dirs, filepath.Dir(real))

default:
// Subcommand, boolean/list flag values, or non-path tokens.
resolvedArgs = append(resolvedArgs, arg)
}
}

return resolvedArgs, dirs
}

// resolvePath returns the real path for p, following symlinks. If the symlink
// target cannot be evaluated (e.g. the path is an output that does not exist
// yet), it falls back to the absolute path.
func resolvePath(p string) string {
if real, err := filepath.EvalSymlinks(p); err == nil {
return real
}
if abs, err := filepath.Abs(p); err == nil {
return abs
}
return p
}

// isExistingFile reports whether arg names an existing regular file. Used to
// distinguish a positional input path from non-path flag values.
func isExistingFile(arg string) bool {
info, err := os.Stat(arg)
if err != nil {
return false
}
return !info.IsDir()
}

// uniqueStrings returns the unique strings from a slice, preserving order.
func uniqueStrings(strs []string) []string {
seen := make(map[string]bool, len(strs))
result := make([]string, 0, len(strs))
for _, s := range strs {
if !seen[s] {
seen[s] = true
result = append(result, s)
}
}
return result
}
43 changes: 29 additions & 14 deletions wasm/private/wasm_optimize.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,31 @@ def _wasm_optimize_impl(ctx):
input_wasm = ctx.file.component
output_wasm = ctx.actions.declare_file(ctx.label.name + ".wasm")

# Get wasmtime toolchain for running loom.wasm
wasmtime_toolchain = ctx.toolchains["@rules_wasm_component//toolchains:wasmtime_toolchain_type"]
wasmtime = wasmtime_toolchain.wasmtime

# Get LOOM WASM component
# Get LOOM WASM component, the wrapper that runs it, and wasmtime.
loom_wasm = ctx.file._loom_wasm

# Build command arguments.
loom_wrapper = ctx.executable._loom_wrapper
wasmtime = ctx.toolchains["@rules_wasm_component//toolchains:wasmtime_toolchain_type"].wasmtime

# Build command arguments for loom_wrapper, which prepends `wasmtime run`
# and the resolved `--dir` preopens (issue #490).
#
# loom reads input paths via WASI, and a fetched/adopted input is staged
# as a symlink whose target escapes a plain `--dir=.` preopen, so wasmtime
# refuses to follow it. The wrapper resolves the symlink on the host and
# preopens the real directory.
#
# The wasmtime binary and loom.wasm are passed as the first two arguments
# rather than located via the wrapper's runfiles: a hardcoded runfiles
# Rlocation embeds the canonical repo name, which differs when
# rules_wasm_component is the root module vs a dependency (the latter gains
# a `rules_wasm_component+` prefix), so the lookup fails for downstream
# consumers. wasmtime opens both files natively, so neither needs a WASI
# mount.
#
# Note: loom v0.3.0 does NOT accept the `--` separator between the wasm
# module path and the subcommand when run under `wasmtime run`.
# TODO: loom reads input paths via WASI and `--dir=.` does not resolve
# the bazel-out symlinks; a small wrapper (mirroring wasmsign2_wrapper)
# is needed to compute real paths and register them as WASI mounts.
args = ctx.actions.args()
args.add("run")
args.add("--dir=.")
args.add(wasmtime)
args.add(loom_wasm)
args.add("optimize")
args.add(input_wasm)
Expand All @@ -52,9 +61,9 @@ def _wasm_optimize_impl(ctx):
args.add("--passes", ",".join(ctx.attr.passes))

ctx.actions.run(
inputs = [input_wasm, loom_wasm],
inputs = [input_wasm, loom_wasm, wasmtime],
outputs = [output_wasm],
executable = wasmtime,
executable = loom_wrapper,
arguments = [args],
mnemonic = "LoomOptimize",
progress_message = "Optimizing WebAssembly component with LOOM: %{label}",
Expand Down Expand Up @@ -120,6 +129,12 @@ advanced, branches, dce, merge-blocks, vacuum, simplify-locals""",
default = "@loom_wasm//file:file",
allow_single_file = True,
),
"_loom_wrapper": attr.label(
doc = "Wrapper that runs loom.wasm under wasmtime with symlink-resolved WASI mounts",
default = "//tools/loom_wrapper",
executable = True,
cfg = "exec",
),
},
toolchains = ["@rules_wasm_component//toolchains:wasmtime_toolchain_type"],
doc = """Optimize a WebAssembly component using LOOM.
Expand Down
Loading