Skip to content

Commit 27eefae

Browse files
committed
fix: eliminate shell scripts from file operations, use hermetic Go tool
Replaces shell script generation in file_ops_actions.bzl with cross-platform hermetic Go binary for all file operations. This eliminates Windows incompatibility and aligns with Bazel-native principles. Changes: - Replace shell scripts with JSON-based hermetic file_ops tool - Update prepare_workspace_action to use file_ops tool - Update setup_js_workspace_action to use file_ops tool - Fix toolchain to point to correct Go binary (not Rust component) - Relax path validation in file_ops tool for Bazel sandbox compatibility - Remove unnecessary go mod download operation Fixes #40
1 parent d2fab66 commit 27eefae

File tree

3 files changed

+90
-137
lines changed

3 files changed

+90
-137
lines changed

toolchains/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ bzl_library(
198198

199199
file_ops_toolchain(
200200
name = "file_ops_toolchain_impl",
201-
file_ops_component = "//tools/file_operations_component:file_ops",
201+
file_ops_component = "//tools/file_ops:file_ops",
202202
wit_files = ["//tools/file_operations_component:wit_files"],
203203
)
204204

tools/bazel_helpers/file_ops_actions.bzl

Lines changed: 83 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -147,127 +147,91 @@ def prepare_workspace_action(ctx, config):
147147
# Create workspace output directory
148148
workspace_dir = ctx.actions.declare_directory(config["work_dir"])
149149

150-
# HERMETIC APPROACH: Use a simple script that only uses POSIX commands available everywhere
151-
# Create a minimal shell script that doesn't depend on system Python
152-
workspace_script = ctx.actions.declare_file(ctx.label.name + "_workspace_setup.sh")
150+
# Get the hermetic file operations tool from toolchain
151+
file_ops_toolchain = ctx.toolchains["@rules_wasm_component//toolchains:file_ops_toolchain_type"]
152+
file_ops_tool = file_ops_toolchain.file_ops_component
153153

154-
# Collect all input files and their destinations
154+
# Collect all input files and build operations list
155155
all_inputs = []
156-
file_mappings = []
156+
operations = []
157157

158158
# Process source files
159159
for source_info in config.get("sources", []):
160160
src_file = source_info["source"]
161161
dest_name = source_info.get("destination") or src_file.basename
162162
all_inputs.append(src_file)
163-
file_mappings.append((src_file, dest_name))
163+
operations.append({
164+
"type": "copy_file",
165+
"src_path": src_file.path,
166+
"dest_path": dest_name,
167+
})
164168

165169
# Process header files
166170
for header_info in config.get("headers", []):
167171
hdr_file = header_info["source"]
168172
dest_name = header_info.get("destination") or hdr_file.basename
169173
all_inputs.append(hdr_file)
170-
file_mappings.append((hdr_file, dest_name))
174+
operations.append({
175+
"type": "copy_file",
176+
"src_path": hdr_file.path,
177+
"dest_path": dest_name,
178+
})
171179

172180
# Process dependency files
173181
for dep_info in config.get("dependencies", []):
174182
dep_file = dep_info["source"]
175183
dest_name = dep_info.get("destination") or dep_file.basename
176184
all_inputs.append(dep_file)
177185

178-
# Include the full dep_info for directory handling
179-
file_mappings.append((dep_file, dest_name, dep_info))
180-
181-
script_lines = [
182-
"#!/bin/sh",
183-
"set -e",
184-
"",
185-
"WORKSPACE_DIR=\"$1\"",
186-
"mkdir -p \"$WORKSPACE_DIR\"",
187-
"",
188-
]
189-
190-
# Add file operations using basic POSIX commands
191-
for mapping in file_mappings:
192-
if len(mapping) == 3:
193-
src_file, dest_name, dep_info = mapping
194-
is_directory = dep_info.get("is_directory", False)
195-
else:
196-
src_file, dest_name = mapping
197-
is_directory = False
198-
199-
# Ensure parent directory exists for nested paths
200-
if "/" in dest_name:
201-
parent_dir = "/".join(dest_name.split("/")[:-1])
202-
script_lines.append("mkdir -p \"$WORKSPACE_DIR/{}\"".format(parent_dir))
203-
204-
# Use cp -r for directories, cp for files
186+
is_directory = dep_info.get("is_directory", False)
205187
if is_directory:
206-
# For directories, copy contents not the directory itself
207-
script_lines.append("mkdir -p \"$WORKSPACE_DIR/{}\"".format(dest_name))
208-
script_lines.append("cp -r \"{}\"/* \"$WORKSPACE_DIR/{}\"".format(src_file.path, dest_name))
188+
operations.append({
189+
"type": "copy_directory_contents",
190+
"src_path": dep_file.path,
191+
"dest_path": dest_name,
192+
})
209193
else:
210-
script_lines.append("cp \"{}\" \"$WORKSPACE_DIR/{}\"".format(src_file.path, dest_name))
194+
operations.append({
195+
"type": "copy_file",
196+
"src_path": dep_file.path,
197+
"dest_path": dest_name,
198+
})
211199

212200
# Add Go module dependency resolution if go_binary provided
213201
go_binary = config.get("go_binary")
214202
if go_binary and config.get("workspace_type") == "go":
215-
# Note: go_binary.path might be relative, ensure it's resolved correctly
216-
go_binary_path = go_binary.path
217-
script_lines.extend([
218-
"",
219-
"# Go module dependency resolution",
220-
"echo \"Resolving Go module dependencies...\"",
221-
"cd \"$WORKSPACE_DIR\"",
222-
"export GOCACHE=\"$WORKSPACE_DIR/.gocache\"",
223-
"export GOPATH=\"$WORKSPACE_DIR/.gopath\"",
224-
"mkdir -p \"$GOCACHE\" \"$GOPATH\"",
225-
"",
226-
"# Find the Go binary in execution root",
227-
"if [ -f \"$EXECROOT/{}\" ]; then".format(go_binary_path),
228-
" GO_BIN=\"$EXECROOT/{}\"".format(go_binary_path),
229-
"elif [ -f \"{}\" ]; then".format(go_binary_path),
230-
" GO_BIN=\"{}\"".format(go_binary_path),
231-
"else",
232-
" echo \"Warning: Go binary not found, skipping dependency resolution\"",
233-
" GO_BIN=\"\"",
234-
"fi",
235-
"",
236-
"if [ -n \"$GO_BIN\" ]; then",
237-
" \"$GO_BIN\" mod download || echo \"Note: Go mod download failed or not needed\"",
238-
" echo \"Go dependencies resolved\"",
239-
"fi",
240-
"",
203+
# Note: go mod download is not needed here as TinyGo handles dependencies
204+
# Just create the cache directories for potential future use
205+
operations.extend([
206+
{"type": "mkdir", "path": ".gocache"},
207+
{"type": "mkdir", "path": ".gopath"},
241208
])
242209

243-
script_lines.extend([
244-
"",
245-
"# Create completion marker",
246-
"echo \"Workspace prepared with {} files\" > \"$WORKSPACE_DIR/.workspace_ready\"".format(len(file_mappings)),
247-
])
210+
# Build JSON config for file operations tool
211+
file_ops_config = {
212+
"workspace_dir": workspace_dir.path,
213+
"operations": operations,
214+
}
248215

216+
# Write config to a JSON file
217+
config_file = ctx.actions.declare_file(ctx.label.name + "_file_ops_config.json")
249218
ctx.actions.write(
250-
output = workspace_script,
251-
content = "\n".join(script_lines),
252-
is_executable = True,
219+
output = config_file,
220+
content = json.encode(file_ops_config),
253221
)
254222

255-
# Add go_binary to inputs if provided
256-
action_inputs = all_inputs + [workspace_script]
257-
if go_binary:
258-
action_inputs.append(go_binary)
259-
260-
# Execute the workspace setup using only basic POSIX shell
223+
# Execute the hermetic file operations tool
261224
ctx.actions.run(
262-
executable = workspace_script,
263-
arguments = [workspace_dir.path],
264-
inputs = action_inputs,
225+
executable = file_ops_tool,
226+
arguments = [config_file.path],
227+
inputs = all_inputs + [config_file],
265228
outputs = [workspace_dir],
266229
mnemonic = "PrepareWorkspaceHermetic",
267230
progress_message = "Preparing {} workspace for {} (hermetic)".format(
268231
config.get("workspace_type", "generic"),
269232
ctx.label,
270233
),
234+
tools = [file_ops_tool],
271235
)
272236

273237
return workspace_dir
@@ -440,69 +404,61 @@ def setup_js_workspace_action(ctx, sources, package_json = None, npm_deps = None
440404
# Create workspace directory
441405
workspace_dir = ctx.actions.declare_directory(ctx.label.name + "_jswork")
442406

443-
# Prepare inputs
444-
all_inputs = list(sources)
445-
if package_json:
446-
all_inputs.append(package_json)
447-
if npm_deps:
448-
all_inputs.append(npm_deps)
407+
# Get the hermetic file operations tool from toolchain
408+
file_ops_toolchain = ctx.toolchains["@rules_wasm_component//toolchains:file_ops_toolchain_type"]
409+
file_ops_tool = file_ops_toolchain.file_ops_component
449410

450-
# Create a shell script that properly copies files (not symlinks)
451-
setup_script = ctx.actions.declare_file(ctx.label.name + "_setup_workspace.sh")
452-
453-
script_lines = [
454-
"#!/bin/bash",
455-
"set -euo pipefail",
456-
"",
457-
"WORKSPACE_DIR=\"$1\"",
458-
"shift",
459-
"",
460-
"# Create workspace directory",
461-
"mkdir -p \"$WORKSPACE_DIR\"",
462-
"echo \"Setting up JavaScript workspace: $WORKSPACE_DIR\"",
463-
"",
464-
]
411+
# Build operations list
412+
all_inputs = []
413+
operations = []
465414

466415
# Copy source files to workspace root (flatten structure)
467416
for src in sources:
468-
script_lines.extend([
469-
"echo \"Copying {} to $WORKSPACE_DIR/{}\"".format(src.path, src.basename),
470-
"cp \"{}\" \"$WORKSPACE_DIR/{}\"".format(src.path, src.basename),
471-
])
417+
all_inputs.append(src)
418+
operations.append({
419+
"type": "copy_file",
420+
"src_path": src.path,
421+
"dest_path": src.basename,
422+
})
472423

473424
if package_json:
474-
script_lines.extend([
475-
"echo \"Copying package.json\"",
476-
"cp \"{}\" \"$WORKSPACE_DIR/package.json\"".format(package_json.path),
477-
])
425+
all_inputs.append(package_json)
426+
operations.append({
427+
"type": "copy_file",
428+
"src_path": package_json.path,
429+
"dest_path": "package.json",
430+
})
478431

479432
if npm_deps:
480-
script_lines.extend([
481-
"echo \"Copying npm dependencies\"",
482-
"cp -r \"{}\" \"$WORKSPACE_DIR/node_modules\"".format(npm_deps.path),
483-
])
433+
all_inputs.append(npm_deps)
434+
operations.append({
435+
"type": "copy_directory_contents",
436+
"src_path": npm_deps.path,
437+
"dest_path": "node_modules",
438+
})
484439

485-
script_lines.extend([
486-
"",
487-
"echo \"JavaScript workspace setup complete\"",
488-
"echo \"Files in workspace:\"",
489-
"ls -la \"$WORKSPACE_DIR\"",
490-
])
440+
# Build JSON config for file operations tool
441+
file_ops_config = {
442+
"workspace_dir": workspace_dir.path,
443+
"operations": operations,
444+
}
491445

446+
# Write config to a JSON file
447+
config_file = ctx.actions.declare_file(ctx.label.name + "_file_ops_config.json")
492448
ctx.actions.write(
493-
output = setup_script,
494-
content = "\n".join(script_lines),
495-
is_executable = True,
449+
output = config_file,
450+
content = json.encode(file_ops_config),
496451
)
497452

498-
# Run the setup script
453+
# Execute the hermetic file operations tool
499454
ctx.actions.run(
500-
executable = setup_script,
501-
arguments = [workspace_dir.path],
502-
inputs = all_inputs,
455+
executable = file_ops_tool,
456+
arguments = [config_file.path],
457+
inputs = all_inputs + [config_file],
503458
outputs = [workspace_dir],
504459
mnemonic = "SetupJSWorkspace",
505460
progress_message = "Setting up JavaScript workspace for %s" % ctx.label,
461+
tools = [file_ops_tool],
506462
)
507463

508464
return workspace_dir

tools/file_ops/main.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -297,19 +297,17 @@ func (r *FileOpsRunner) validateConfig() error {
297297
return fmt.Errorf("workspace_dir cannot be empty")
298298
}
299299

300-
if !filepath.IsAbs(r.config.WorkspaceDir) {
301-
return fmt.Errorf("workspace_dir must be an absolute path: %s", r.config.WorkspaceDir)
302-
}
300+
// Note: In Bazel sandbox, paths may be relative to execution root
301+
// Bazel handles path resolution, so we don't strictly require absolute paths
303302

304303
for i, op := range r.config.Operations {
305304
switch op.Type {
306305
case "copy_file":
307306
if op.SrcPath == "" || op.DestPath == "" {
308307
return fmt.Errorf("operation %d: copy_file requires src_path and dest_path", i)
309308
}
310-
if !filepath.IsAbs(op.SrcPath) {
311-
return fmt.Errorf("operation %d: src_path must be absolute: %s", i, op.SrcPath)
312-
}
309+
// Note: src_path can be Bazel-relative (e.g., bazel-out/...)
310+
// dest_path should be relative to workspace
313311
if filepath.IsAbs(op.DestPath) {
314312
return fmt.Errorf("operation %d: dest_path must be relative: %s", i, op.DestPath)
315313
}
@@ -324,9 +322,8 @@ func (r *FileOpsRunner) validateConfig() error {
324322
if op.SrcPath == "" || op.DestPath == "" {
325323
return fmt.Errorf("operation %d: copy_directory_contents requires src_path and dest_path", i)
326324
}
327-
if !filepath.IsAbs(op.SrcPath) {
328-
return fmt.Errorf("operation %d: src_path must be absolute: %s", i, op.SrcPath)
329-
}
325+
// Note: src_path can be Bazel-relative (e.g., bazel-out/...)
326+
// dest_path should be relative to workspace
330327
if filepath.IsAbs(op.DestPath) {
331328
return fmt.Errorf("operation %d: dest_path must be relative: %s", i, op.DestPath)
332329
}

0 commit comments

Comments
 (0)