blob: 56a84e1102f2957a41a0f7239a3904e40a000d5c [file] [log] [blame]
#!/bin/bash
# Quickly compile and run C/C++ snippets against IREE runtime.
#
# Usage: iree-bazel-try [options] [files...] [-- program-args...]
#
# Examples:
# iree-bazel-try snippet.c
# echo 'int main() { return 0; }' | iree-bazel-try
# iree-bazel-try -e 'int main() { return 42; }' -c
# iree-bazel-try --dep //runtime/src/iree/vm test.c -- --help
set -e
# Source shared library.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/iree-bazel-lib"
iree_bazel_init "iree-bazel-try"
# Expand combined short flags (e.g., -nv -> -n -v).
eval "set -- $(iree_expand_combined_flags "$@")"
show_help() {
cat << 'EOF'
iree-bazel-try - Compile and run C/C++ snippets against IREE runtime
Automatically infers dependencies from #include directives. Just write code
with IREE headers and it figures out what deps are needed.
USAGE
iree-bazel-try [options] [files...] [-- program-args...]
OPTIONS
-n, --dry_run Show the bazel command without executing
-v, --verbose Show bazel commands, inferred deps, and temp paths
-h, --help Show this help
NOTE: Short flags can be combined: -nv is equivalent to -n -v
INPUT (at least one required)
files... C/C++ source files (multiple allowed)
-e, --execute=CODE Inline code (repeatable, concatenated)
(stdin) Reads from stdin if no files and no -e
LANGUAGE
-x c Force C language (default for stdin)
-x c++ Force C++ language
COMPILATION
--config=NAME Bazel config (debug, asan, msan, tsan)
--features=NAME Bazel feature (thin_lto, etc; repeatable)
--copt=FLAG Compiler flag for all targets (repeatable)
--linkopt=FLAG Linker flag for all targets (repeatable)
--dep=LABEL Add explicit Bazel dependency (repeatable)
--no_infer Disable automatic dependency inference
--retry=N Max build retries for dep fixing (default: 3)
OUTPUT
-c, --compile_only Compile but don't run (exit 0 on success)
-o, --output PATH Copy binary to PATH after build
DEBUG
--keep Keep temp directory for inspection
CACHE MANAGEMENT
--clean Remove all cached data and exit
--cache_status Show cache usage statistics and exit
EXIT CODES
0 Success (build/run completed)
1 Build failed (compiler errors, missing deps)
N Program exit code (when running)
===============================================================================
QUICK EXPLORATION - Use C like you'd use Python
===============================================================================
The primary use case: quickly answer "what does this function do?" without
writing BUILD files or hunting for dependencies.
EXAMPLE: "What does iree_unicode_utf8_decode return for an emoji?"
$ iree-bazel-try -e '
#include "iree/base/internal/unicode.h"
#include <stdio.h>
int main() {
const char* emoji = "\xF0\x9F\x98\x80"; // U+1F600 grinning face
iree_string_view_t text = iree_make_cstring_view(emoji);
iree_host_size_t pos = 0;
uint32_t cp = iree_unicode_utf8_decode(text, &pos);
printf("Codepoint: U+%04X, bytes consumed: %zu\n", cp, pos);
return 0;
}'
Codepoint: U+1F600, bytes consumed: 4
===============================================================================
MORE EXAMPLES
===============================================================================
EXAMPLE: Minimal test (stdin)
$ echo 'int main() { return 0; }' | iree-bazel-try
(no output, exit code 0)
EXAMPLE: Using IREE APIs (auto-infers deps)
$ iree-bazel-try -e '#include "iree/base/api.h"
#include <stdio.h>
int main() {
iree_status_t s = iree_ok_status();
printf("ok: %d\n", iree_status_is_ok(s));
return 0;
}'
ok: 1
EXAMPLE: Compile-only mode
$ iree-bazel-try -c -e 'int main() { return 0; }'
(exit code 0)
EXAMPLE: Pass arguments to program
$ iree-bazel-try -e '#include <stdio.h>
int main(int argc, char** argv) {
for (int i = 0; i < argc; i++) printf("%d: %s\n", i, argv[i]);
return 0;
}' -- --flag value
0: /path/to/.../snippet
1: --flag
2: value
EXAMPLE: Multiple source files
$ echo 'int helper() { return 42; }' > helper.c
$ echo 'extern int helper(); int main() { return helper() - 42; }' > main.c
$ iree-bazel-try main.c helper.c
(exit code 0)
EXAMPLE: C++ with custom flags (auto-detected from includes or use -x c++)
$ iree-bazel-try --copt=-std=c++17 -e '
#include <optional>
#include <stdio.h>
int main() {
std::optional<int> x = 42;
printf("%d\n", *x);
return 0;
}'
42
EXAMPLE: Build with AddressSanitizer
$ iree-bazel-try --config=asan -e '
#include <stdlib.h>
int main() {
int* p = malloc(4);
free(p);
return *p; // use-after-free
}'
==ERROR: AddressSanitizer: heap-use-after-free...
EXAMPLE: Verbose mode (shows deps and commands)
$ iree-bazel-try -v -c -e '#include "iree/hal/api.h"
int main() { return 0; }'
[iree-bazel-try] Inferred deps: //runtime/src/iree/hal
[iree-bazel-try] Building //.iree-bazel-try/...:snippet...
===============================================================================
MLIR TRANSFORM HARNESS - Quick compiler experiments
===============================================================================
For one-shot MLIR transforms without modifying the compiler. C++ auto-detected.
EXAMPLE: Walk operations (analysis)
$ echo 'func.func @f() { return }' | iree-bazel-try -e '
#include "iree/compiler/Tools/MlirTransformHarness.h"
void xform(ModuleOp m) {
m.walk([](Operation *op) { llvm::outs() << op->getName() << "\n"; });
}
MLIR_TRANSFORM_MAIN_NO_PRINT(xform)
'
func.return
func.func
builtin.module
EXAMPLE: Pattern rewrite (transform)
$ echo 'func.func @f(%a: i32) -> i32 {
%c0 = arith.constant 0 : i32
%r = arith.addi %a, %c0 : i32
return %r : i32
}' | iree-bazel-try -e '
#include "iree/compiler/Tools/MlirTransformHarness.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
struct AddZero : OpRewritePattern<arith::AddIOp> {
using OpRewritePattern::OpRewritePattern;
LogicalResult matchAndRewrite(arith::AddIOp op, PatternRewriter &rw) const override {
if (auto c = op.getRhs().getDefiningOp<arith::ConstantIntOp>())
if (c.value() == 0) { rw.replaceOp(op, op.getLhs()); return success(); }
return failure();
}
};
MLIR_PATTERN_MAIN(AddZero)
'
(outputs transformed IR with addi removed)
===============================================================================
GTEST HARNESS - Quick runtime tests
===============================================================================
Run gtests without BUILD files. Includes status matchers. C++ auto-detected.
EXAMPLE: Test with status matchers
$ iree-bazel-try -e '
#include "iree/testing/gtest_harness.h"
TEST(StatusTest, OkStatus) {
IREE_EXPECT_OK(iree_ok_status());
EXPECT_THAT(iree_ok_status(), IsOk());
}
TEST(StatusTest, ErrorStatus) {
EXPECT_THAT(iree_make_status(IREE_STATUS_INVALID_ARGUMENT),
StatusIs(StatusCode::kInvalidArgument));
}
'
===============================================================================
GBENCHMARK HARNESS - Quick benchmarks
===============================================================================
Run Google Benchmark microbenchmarks. C++ auto-detected.
EXAMPLE: Benchmark memory allocation
$ iree-bazel-try -e '
#include "iree/testing/gbenchmark_harness.h"
void BM_Alloc(benchmark::State& state) {
for (auto _ : state) {
void* p = NULL;
iree_allocator_malloc(iree_allocator_system(), 1024, &p);
DoNotOptimize(p);
iree_allocator_free(iree_allocator_system(), p);
}
}
BENCHMARK(BM_Alloc);
'
===============================================================================
AUTOMATIC DEPENDENCY INFERENCE
===============================================================================
The tool automatically scans source files for #include "iree/..." directives
and infers the corresponding Bazel dependencies:
#include "iree/base/api.h" -> //runtime/src/iree/base
#include "iree/hal/buffer.h" -> //runtime/src/iree/hal
#include "iree/vm/module.h" -> //runtime/src/iree/vm
#include "iree/io/file_handle.h" -> //runtime/src/iree/io
If the build fails with missing symbols, the tool will attempt to find the
correct dependency using bazel query and retry the build (up to --retry times).
Use --no_infer to disable this and rely only on explicit --dep flags.
AI AGENT INTEGRATION
--agent-md Print a concise snippet for CLAUDE.md/AGENT.md files
SEE ALSO
iree-bazel-build, iree-bazel-test, iree-bazel-run
EOF
}
# Maximum number of cache slots to keep (LRU eviction beyond this).
MAX_CACHE_SLOTS=8
# Portable hash command: use md5sum on Linux, md5 on macOS/BSD.
_portable_hash() {
if command -v md5sum &>/dev/null; then
md5sum | cut -c1-12
else
md5 -r | cut -c1-12
fi
}
# Portable file locking using mkdir (atomic on all POSIX systems).
# flock is Linux-specific; mkdir-based locking works everywhere.
#
# Stale lock detection: if the process that created the lock is dead,
# we reclaim the lock immediately rather than waiting forever.
_acquire_lock() {
local lock_dir="${1}.d"
local timeout="${2:-60}"
local waited=0
while true; do
# Try to atomically create lock directory.
if mkdir "${lock_dir}" 2>/dev/null; then
# Successfully acquired lock - write our PID.
echo $$ > "${lock_dir}/pid"
return 0
fi
# Lock exists - check if holder is still alive.
if [[ -f "${lock_dir}/pid" ]]; then
local holder_pid
holder_pid=$(cat "${lock_dir}/pid" 2>/dev/null)
if [[ -n "${holder_pid}" ]]; then
# kill -0 checks if process exists without sending signal.
if ! kill -0 "${holder_pid}" 2>/dev/null; then
# Holder process is dead - reclaim the stale lock.
iree_debug "Reclaiming stale lock from dead process ${holder_pid}"
rm -rf "${lock_dir}"
continue # Retry acquisition immediately.
fi
fi
elif [[ -d "${lock_dir}" ]]; then
# Lock directory exists but no PID file - likely a very old stale lock
# or mid-creation crash. Give it a moment, then reclaim.
if [[ "${waited}" -gt 2 ]]; then
iree_debug "Reclaiming orphaned lock directory (no PID file)"
rm -rf "${lock_dir}"
continue
fi
fi
# Lock is held by a live process (or we're giving it a grace period).
sleep 1
waited=$((waited + 1))
if [[ "${waited}" -ge "${timeout}" ]]; then
# Provide actionable error message.
local msg="Timeout waiting for lock on cache slot"
if [[ -f "${lock_dir}/pid" ]]; then
local holder_pid
holder_pid=$(cat "${lock_dir}/pid" 2>/dev/null)
msg="${msg} (held by PID ${holder_pid})"
fi
iree_warn "${msg}"
return 1
fi
done
}
_release_lock() {
local lock_dir="${1}.d"
rm -rf "${lock_dir}"
}
# Clean up any stale lock directories in the cache.
# Called during cache maintenance to prevent lock accumulation.
_cleanup_stale_locks() {
local cache_dir="${1}"
if [[ ! -d "${cache_dir}" ]]; then
return
fi
# Find all .lock.d directories.
while IFS= read -r lock_dir; do
[[ -z "${lock_dir}" ]] && continue
if [[ -f "${lock_dir}/pid" ]]; then
local holder_pid
holder_pid=$(cat "${lock_dir}/pid" 2>/dev/null)
if [[ -n "${holder_pid}" ]] && ! kill -0 "${holder_pid}" 2>/dev/null; then
iree_debug "Cleaning stale lock from dead process ${holder_pid}"
rm -rf "${lock_dir}"
fi
elif [[ -d "${lock_dir}" ]]; then
# No PID file - orphaned lock.
iree_debug "Cleaning orphaned lock directory: ${lock_dir}"
rm -rf "${lock_dir}"
fi
done < <(find "${cache_dir}" -maxdepth 1 -name "*.lock.d" -type d 2>/dev/null)
}
# Compute a hash for cache slot naming based on deps-affecting inputs.
# Hash is based on #includes + explicit deps + build options + language.
# File inputs also include content hash to avoid stale builds.
# This allows multiple runs with same deps config to share the cached BUILD.bazel.
# Different build flags (copts, linkopts, features) get separate cache slots so Bazel's
# action cache doesn't thrash when alternating between flag combinations.
# Args: source_files array, extra_deps array, copts array, linkopts array, language,
# bazel_user_flags array, include_contents(0/1)
compute_cache_hash() {
local -n _source_files=${1}
local -n _extra_deps=${2}
local -n _copts=${3}
local -n _linkopts=${4}
local _language=${5:-c}
local -n _bazel_user_flags=${6}
local _include_contents=${7:-0}
{
# Hash language (determines file extension and compilation mode).
printf 'language:%s\n' "${_language}"
# Hash #include directives (determines inferred deps).
printf 'includes:\n'
for src in "${_source_files[@]}"; do
grep -h '#include\s*"[^"]*"' "${src}" 2>/dev/null | \
sed 's/.*#include\s*"\([^"]*\)".*/\1/' | \
grep -E '^(iree/|llvm/|mlir/)'
done | sort -u
# Hash explicit deps (user-provided --dep flags).
printf 'extra_deps:\n'
printf '%s\n' "${_extra_deps[@]}" | sort -u
# Hash build flags that affect compilation output.
printf 'copts:\n'
printf '%s\n' "${_copts[@]}" | sort -u
printf 'linkopts:\n'
printf '%s\n' "${_linkopts[@]}" | sort -u
printf 'bazel_flags:\n'
printf '%s\n' "${_bazel_user_flags[@]}" | sort -u
if [[ "${_include_contents}" == "1" ]]; then
printf 'contents:\n'
for src in "${_source_files[@]}"; do
if [[ -f "${src}" ]]; then
printf '%s:%s\n' "$(basename "${src}")" "$(cat "${src}" | _portable_hash)"
fi
done | sort -u
fi
} | _portable_hash
}
# Ensure cache doesn't exceed MAX_CACHE_SLOTS by evicting oldest slots.
# Uses mtime for LRU ordering (we touch slots on use).
# Also cleans up any stale locks from crashed processes.
enforce_cache_limit() {
local cache_dir="${1}"
if [[ ! -d "${cache_dir}" ]]; then
return
fi
# First, clean up any stale lock directories.
_cleanup_stale_locks "${cache_dir}"
# Count only actual cache slot directories (not .lock.d directories).
local slot_count
slot_count=$(find "${cache_dir}" -mindepth 1 -maxdepth 1 -type d ! -name "*.lock.d" 2>/dev/null | wc -l)
if [[ "${slot_count}" -gt "${MAX_CACHE_SLOTS}" ]]; then
local excess=$((slot_count - MAX_CACHE_SLOTS))
iree_debug "Cache has ${slot_count} slots, evicting ${excess} oldest"
# Find oldest directories by mtime and remove them (exclude lock dirs).
find "${cache_dir}" -mindepth 1 -maxdepth 1 -type d ! -name "*.lock.d" -printf '%T@ %p\n' 2>/dev/null | \
sort -n | head -n "${excess}" | cut -d' ' -f2- | \
while read -r old_slot; do
rm -rf "${old_slot}"
iree_debug "Evicted cache slot: ${old_slot}"
done
fi
}
# Show cache usage statistics.
show_cache_status() {
local base_dir="${1}"
local cache_dir="${base_dir}/cache"
local output_base="${base_dir}/output_base"
echo "iree-bazel-try cache status"
echo "==========================="
echo ""
echo "Base directory: ${base_dir}"
echo ""
# Cache slots.
if [[ -d "${cache_dir}" ]]; then
local slot_count
slot_count=$(find "${cache_dir}" -mindepth 1 -maxdepth 1 -type d ! -name "*.lock.d" 2>/dev/null | wc -l)
local slot_size
slot_size=$(du -sh "${cache_dir}" 2>/dev/null | cut -f1)
echo "Cache slots: ${slot_count}/${MAX_CACHE_SLOTS} (${slot_size})"
# List slots with age.
if [[ "${slot_count}" -gt 0 ]]; then
echo ""
echo "Slots (newest first):"
local now
now=$(date +%s)
find "${cache_dir}" -mindepth 1 -maxdepth 1 -type d ! -name "*.lock.d" -printf '%T@ %f\n' 2>/dev/null | \
sort -rn | \
while read -r mtime slot; do
local age_seconds=$((now - ${mtime%.*}))
local age_str
if [[ "${age_seconds}" -lt 60 ]]; then
age_str="${age_seconds}s ago"
elif [[ "${age_seconds}" -lt 3600 ]]; then
age_str="$((age_seconds / 60))m ago"
elif [[ "${age_seconds}" -lt 86400 ]]; then
age_str="$((age_seconds / 3600))h ago"
else
age_str="$((age_seconds / 86400))d ago"
fi
local slot_size
slot_size=$(du -sh "${cache_dir}/${slot}" 2>/dev/null | cut -f1)
echo " ${slot} (${slot_size}, ${age_str})"
done
fi
# Stale locks.
local lock_count
lock_count=$(find "${cache_dir}" -maxdepth 1 -name "*.lock.d" -type d 2>/dev/null | wc -l)
if [[ "${lock_count}" -gt 0 ]]; then
echo ""
echo "Lock directories: ${lock_count}"
find "${cache_dir}" -maxdepth 1 -name "*.lock.d" -type d 2>/dev/null | \
while read -r lock_dir; do
local status="active"
if [[ -f "${lock_dir}/pid" ]]; then
local holder_pid
holder_pid=$(cat "${lock_dir}/pid" 2>/dev/null)
if [[ -n "${holder_pid}" ]] && ! kill -0 "${holder_pid}" 2>/dev/null; then
status="STALE (PID ${holder_pid} dead)"
else
status="held by PID ${holder_pid}"
fi
else
status="ORPHANED (no PID)"
fi
echo " $(basename "${lock_dir}"): ${status}"
done
fi
else
echo "Cache slots: 0/${MAX_CACHE_SLOTS} (not created yet)"
fi
echo ""
# Bazel output_base (can be large).
if [[ -d "${output_base}" ]]; then
local output_size
output_size=$(du -sh "${output_base}" 2>/dev/null | cut -f1)
echo "Bazel output_base: ${output_size}"
echo " (This is used when --copt/--features are specified)"
else
echo "Bazel output_base: not created"
fi
echo ""
# Total size.
if [[ -d "${base_dir}" ]]; then
local total_size
total_size=$(du -sh "${base_dir}" 2>/dev/null | cut -f1)
echo "Total: ${total_size}"
fi
}
# Clean all cached data.
clean_cache() {
local base_dir="${1}"
if [[ ! -d "${base_dir}" ]]; then
echo "No cache directory to clean."
return 0
fi
echo "Cleaning iree-bazel-try cache..."
# Show what we're about to clean.
local total_size
total_size=$(du -sh "${base_dir}" 2>/dev/null | cut -f1)
echo " Directory: ${base_dir}"
echo " Size: ${total_size}"
# Clean staging directories (from any crashed processes).
local staging_count
staging_count=$(find "${base_dir}" -maxdepth 1 -name "staging_*" -type d 2>/dev/null | wc -l)
if [[ "${staging_count}" -gt 0 ]]; then
echo " Removing ${staging_count} staging directories..."
find "${base_dir}" -maxdepth 1 -name "staging_*" -type d -exec rm -rf {} \; 2>/dev/null
fi
# Clean cache slots (includes stale locks).
if [[ -d "${base_dir}/cache" ]]; then
local slot_count
slot_count=$(find "${base_dir}/cache" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)
echo " Removing ${slot_count} cache entries..."
rm -rf "${base_dir}/cache"
fi
# Clean Bazel output_base.
if [[ -d "${base_dir}/output_base" ]]; then
local output_size
output_size=$(du -sh "${base_dir}/output_base" 2>/dev/null | cut -f1)
echo " Removing Bazel output_base (${output_size})..."
rm -rf "${base_dir}/output_base"
fi
# Clean bazel-* symlinks.
for symlink in "${base_dir}"/bazel-*; do
if [[ -L "${symlink}" ]]; then
echo " Removing symlink: $(basename "${symlink}")"
rm -f "${symlink}"
fi
done
echo "Done."
}
# Infer Bazel deps from #include directives in source files.
# Uses bazel query attr(hdrs, pattern) to find targets that export each header.
# Outputs one dep per line.
infer_deps_from_sources() {
local -a sources=("$@")
local BAZEL_BIN
BAZEL_BIN=$(iree_get_bazel_command)
local -a runtime_patterns=()
local -a compiler_patterns=()
local -a llvm_files=()
local -a mlir_files=()
# Collect all headers from all source files.
for src in "${sources[@]}"; do
if [[ ! -f "${src}" ]]; then
continue
fi
# Extract all #include "..." directives.
while IFS= read -r header; do
local dir_path="${header%/*}"
local filename="${header##*/}"
case "${header}" in
iree/compiler/*)
# Pattern: iree/compiler/Foo/Bar:file.h (use path for specificity).
compiler_patterns+=("${dir_path}:${filename}")
;;
iree/*)
# Pattern: iree/base:api.h or iree/base/internal:unicode.h.
runtime_patterns+=("${dir_path}:${filename}")
;;
llvm/*)
# LLVM uses different structure, just use filename.
llvm_files+=("${filename}")
;;
mlir/*)
# MLIR uses different structure, just use filename.
mlir_files+=("${filename}")
;;
esac
done < <(grep -h '#include\s*"[^"]*"' "${src}" 2>/dev/null | \
sed 's/.*#include\s*"\([^"]*\)".*/\1/' | \
grep -E '^(iree/|llvm/|mlir/)')
done
# Query each category using attr(hdrs, pattern) - handles all package structures.
local -a all_deps=()
if [[ ${#runtime_patterns[@]} -gt 0 ]]; then
iree_debug "Querying deps for ${#runtime_patterns[@]} runtime headers..."
local pattern
pattern=$(printf '%s\n' "${runtime_patterns[@]}" | sed 's/\./\\./g' | sort -u | paste -sd'|' -)
while IFS= read -r dep; do
[[ -n "${dep}" ]] && all_deps+=("${dep}")
done < <("${BAZEL_BIN}" query "attr(hdrs, '(${pattern})', //runtime/src/iree/...)" 2>/dev/null 9>&-)
fi
if [[ ${#compiler_patterns[@]} -gt 0 ]]; then
iree_debug "Querying deps for ${#compiler_patterns[@]} compiler headers..."
local pattern
pattern=$(printf '%s\n' "${compiler_patterns[@]}" | sed 's/\./\\./g' | sort -u | paste -sd'|' -)
while IFS= read -r dep; do
[[ -n "${dep}" ]] && all_deps+=("${dep}")
done < <("${BAZEL_BIN}" query "attr(hdrs, '(${pattern})', //compiler/src/iree/compiler/...)" 2>/dev/null 9>&-)
fi
if [[ ${#llvm_files[@]} -gt 0 ]]; then
iree_debug "Querying deps for ${#llvm_files[@]} LLVM headers..."
local pattern
pattern=$(printf '%s\n' "${llvm_files[@]}" | sed 's/\./\\./g' | sort -u | paste -sd'|' -)
while IFS= read -r dep; do
[[ -n "${dep}" ]] && all_deps+=("${dep}")
done < <("${BAZEL_BIN}" query "attr(hdrs, '(${pattern})', @llvm-project//llvm/...)" 2>/dev/null 9>&-)
fi
if [[ ${#mlir_files[@]} -gt 0 ]]; then
iree_debug "Querying deps for ${#mlir_files[@]} MLIR headers..."
local pattern
pattern=$(printf '%s\n' "${mlir_files[@]}" | sed 's/\./\\./g' | sort -u | paste -sd'|' -)
while IFS= read -r dep; do
[[ -n "${dep}" ]] && all_deps+=("${dep}")
done < <("${BAZEL_BIN}" query "attr(hdrs, '(${pattern})', @llvm-project//mlir/...)" 2>/dev/null 9>&-)
fi
# Output unique deps.
printf '%s\n' "${all_deps[@]}" | sort -u
}
# Try to find a dep for a missing symbol using bazel query.
# This is called when build fails.
find_dep_for_symbol() {
local symbol="${1}"
local worktree="${2}"
# Search for the symbol in IREE runtime sources.
local matches
matches=$(grep -rl "^[^/]*\b${symbol}\b" "${worktree}/runtime/src/iree/" 2>/dev/null | head -5)
for match in ${matches}; do
# Convert file path to bazel package.
local rel_path="${match#${worktree}/}"
local dir_path="${rel_path%/*}"
echo "//${dir_path}"
return 0
done
return 1
}
# Parse build errors and suggest missing deps.
# Returns deps to add, one per line.
parse_build_errors() {
local log_file="${1}"
local worktree="${2}"
local -A suggested_deps
# Look for undefined reference errors.
while IFS= read -r symbol; do
if [[ -n "${symbol}" ]]; then
local dep
if dep=$(find_dep_for_symbol "${symbol}" "${worktree}" 2>/dev/null); then
if [[ -z "${suggested_deps[${dep}]:-}" ]]; then
suggested_deps[${dep}]=1
echo "${dep}"
fi
fi
fi
done < <(grep -o "undefined reference to \`[^']*'" "${log_file}" 2>/dev/null | \
sed "s/undefined reference to \`\\([^']*\\)'/\\1/")
# Look for missing header errors.
while IFS= read -r header; do
if [[ "${header}" == iree/* ]]; then
local package="${header%/*}"
# Simplify to top-level package.
local component="${package#iree/}"
component="${component%%/*}"
local dep="//runtime/src/iree/${component}"
if [[ -z "${suggested_deps[${dep}]:-}" ]]; then
suggested_deps[${dep}]=1
echo "${dep}"
fi
fi
done < <(grep -o "fatal error: '[^']*' file not found" "${log_file}" 2>/dev/null | \
sed "s/fatal error: '\\([^']*\\)' file not found/\\1/")
}
# Main script.
DRY_RUN=0
COMPILE_ONLY=0
KEEP_TEMP=0
INFER_DEPS=1
MAX_RETRIES=3
OUTPUT_PATH=""
LANGUAGE=""
INLINE_CODE=""
declare -a SOURCE_FILES=()
declare -a EXTRA_DEPS=()
declare -a COPTS=()
declare -a LINKOPTS=()
declare -a BAZEL_ARGS=()
declare -a BAZEL_USER_FLAGS=()
declare -a PROGRAM_ARGS=()
# Parse arguments.
PARSING_PROGRAM_ARGS=0
while [[ $# -gt 0 ]]; do
if [[ "${PARSING_PROGRAM_ARGS}" == "1" ]]; then
PROGRAM_ARGS+=("${1}")
shift
continue
fi
case "${1}" in
-h|--help)
show_help
exit 0
;;
--agent-md|--agent_md)
iree_show_agent_md
exit 0
;;
-n|--dry_run|--dry-run)
DRY_RUN=1
shift
;;
-v|--verbose)
IREE_BAZEL_VERBOSE=1
shift
;;
-c|--compile_only|--compile_only)
COMPILE_ONLY=1
shift
;;
--keep)
KEEP_TEMP=1
shift
;;
--clean)
# Find worktree root first.
iree_require_worktree
clean_cache "${IREE_BAZEL_WORKTREE_DIR}/.iree-bazel-try"
exit 0
;;
--cache-status|--cache_status)
# Find worktree root first.
iree_require_worktree
show_cache_status "${IREE_BAZEL_WORKTREE_DIR}/.iree-bazel-try"
exit 0
;;
--no_infer|--no_infer)
INFER_DEPS=0
shift
;;
--retry)
MAX_RETRIES="${2}"
shift 2
;;
--retry=*)
MAX_RETRIES="${1#--retry=}"
shift
;;
-o|--output)
OUTPUT_PATH="${2}"
shift 2
;;
--output=*)
OUTPUT_PATH="${1#--output=}"
shift
;;
-x)
LANGUAGE="${2}"
shift 2
;;
-e|--execute)
INLINE_CODE="${INLINE_CODE}${2}"$'\n'
shift 2
;;
--execute=*)
INLINE_CODE="${INLINE_CODE}${1#--execute=}"$'\n'
shift
;;
--dep)
EXTRA_DEPS+=("${2}")
shift 2
;;
--dep=*)
EXTRA_DEPS+=("${1#--dep=}")
shift
;;
--copt)
COPTS+=("${2}")
shift 2
;;
--copt=*)
COPTS+=("${1#--copt=}")
shift
;;
--linkopt)
LINKOPTS+=("${2}")
shift 2
;;
--linkopt=*)
LINKOPTS+=("${1#--linkopt=}")
shift
;;
--features)
BAZEL_ARGS+=("--features=${2}")
BAZEL_USER_FLAGS+=("--features=${2}")
shift 2
;;
--config=*|--features=*)
BAZEL_ARGS+=("${1}")
BAZEL_USER_FLAGS+=("${1}")
shift
;;
--)
PARSING_PROGRAM_ARGS=1
shift
;;
-*)
iree_error "Unknown option: ${1}"
echo "Use --help for usage information."
exit 1
;;
*)
SOURCE_FILES+=("${1}")
shift
;;
esac
done
# Find worktree root.
iree_require_worktree
cd "${IREE_BAZEL_WORKTREE_DIR}"
iree_ensure_configured
# Prepend default configs to bazel args.
iree_bazel_build_default_configs
BAZEL_ARGS=("${IREE_BAZEL_DEFAULT_CONFIGS[@]}" "${BAZEL_ARGS[@]}")
# Propagate copts and linkopts to bazel command line so they apply to all
# transitive dependencies, not just the snippet target.
for opt in "${COPTS[@]}"; do
BAZEL_ARGS+=("--copt=${opt}")
done
for opt in "${LINKOPTS[@]}"; do
BAZEL_ARGS+=("--linkopt=${opt}")
done
# Auto-fix ThinLTO backend compile issue: Bazel passes linker flags to the
# compiler during LTO backend compile, causing -Werror to fire on unused
# linker arguments. Suppress this automatically.
declare -a BAZEL_STARTUP_ARGS=()
HAS_CUSTOM_FLAGS=0
for flag in "${BAZEL_USER_FLAGS[@]}"; do
if [[ "${flag}" == "--features=thin_lto" ]]; then
BAZEL_ARGS+=("--copt=-Wno-unused-command-line-argument")
break
fi
done
if [[ ${#COPTS[@]} -gt 0 || ${#LINKOPTS[@]} -gt 0 || ${#BAZEL_USER_FLAGS[@]} -gt 0 ]]; then
HAS_CUSTOM_FLAGS=1
fi
# Use a separate output_base when custom flags are present to avoid polluting
# the main Bazel server's analysis cache. This ensures that running
# iree-bazel-try with --copt/--features doesn't cause config invalidation
# for subsequent iree-bazel-build/test/run invocations.
# Also redirect workspace symlinks to avoid overwriting the main bazel-bin.
if [[ "${HAS_CUSTOM_FLAGS}" == "1" ]]; then
BAZEL_TRY_OUTPUT_BASE="${IREE_BAZEL_WORKTREE_DIR}/.iree-bazel-try/output_base"
mkdir -p "${BAZEL_TRY_OUTPUT_BASE}"
BAZEL_STARTUP_ARGS+=("--output_base=${BAZEL_TRY_OUTPUT_BASE}")
BAZEL_ARGS+=("--symlink_prefix=${IREE_BAZEL_WORKTREE_DIR}/.iree-bazel-try/bazel-")
iree_debug "Using separate output_base for custom flags: ${BAZEL_TRY_OUTPUT_BASE}"
fi
# Ensure helper functions use the same Bazel startup args (output_base, etc.).
iree_bazel_set_startup_args "${BAZEL_STARTUP_ARGS[@]}"
# Determine input source.
HAS_STDIN=0
if [[ -z "${INLINE_CODE}" ]] && [[ ${#SOURCE_FILES[@]} -eq 0 ]]; then
# Check if stdin has data.
if [[ -t 0 ]]; then
iree_error "No input provided. Specify files, use -e, or pipe to stdin."
echo "Use --help for usage information."
exit 1
fi
HAS_STDIN=1
fi
# Set up base directory (ignored via .gitignore).
TRY_BASE="${IREE_BAZEL_WORKTREE_DIR}/.iree-bazel-try"
CACHE_DIR="${TRY_BASE}/cache"
STAGING_DIR="${TRY_BASE}/staging_$$"
# Create staging directory for source files (needed for deps inference).
mkdir -p "${STAGING_DIR}"
# Cleanup staging on exit (cache slots are kept).
# Note: trap is set later, after lock acquisition, to include lock release.
cleanup_staging() {
if [[ -n "${STAGING_DIR}" ]] && [[ -d "${STAGING_DIR}" ]]; then
rm -rf "${STAGING_DIR}"
fi
}
# Collect source files in staging directory.
declare -a BUILD_SRCS=()
declare -a STAGED_SOURCE_PATHS=()
declare -a HASH_SOURCE_PATHS=()
HAS_FILE_INPUT=0
# Read stdin into variable if needed (so we can scan it for auto-detection).
STDIN_CONTENT=""
if [[ "${HAS_STDIN}" == "1" ]]; then
STDIN_CONTENT=$(cat)
fi
# Auto-detect C++ from includes if language not explicitly set.
# C++-only headers: llvm/, mlir/, iree/compiler/, iree/testing/, gtest/, gmock/, benchmark/
ALL_CODE="${INLINE_CODE}${STDIN_CONTENT}"
if [[ -z "${LANGUAGE}" ]]; then
if echo "${ALL_CODE}" | grep -qE '#include\s*[<"](llvm/|mlir/|iree/compiler/|iree/testing/|gtest/|gmock/|benchmark/)'; then
LANGUAGE="c++"
iree_debug "Auto-detected C++ from includes"
else
LANGUAGE="c"
fi
fi
# Handle inline code.
if [[ -n "${INLINE_CODE}" ]]; then
if [[ "${LANGUAGE}" == "c++" ]]; then
INLINE_FILE="${STAGING_DIR}/inline.cc"
else
INLINE_FILE="${STAGING_DIR}/inline.c"
fi
printf '%s' "${INLINE_CODE}" > "${INLINE_FILE}"
BUILD_SRCS+=("$(basename "${INLINE_FILE}")")
STAGED_SOURCE_PATHS+=("${INLINE_FILE}")
HASH_SOURCE_PATHS+=("${INLINE_FILE}")
iree_debug "Wrote inline code to ${INLINE_FILE}"
fi
# Handle stdin.
if [[ -n "${STDIN_CONTENT}" ]]; then
if [[ "${LANGUAGE}" == "c++" ]]; then
STDIN_FILE="${STAGING_DIR}/stdin.cc"
else
STDIN_FILE="${STAGING_DIR}/stdin.c"
fi
printf '%s' "${STDIN_CONTENT}" > "${STDIN_FILE}"
BUILD_SRCS+=("$(basename "${STDIN_FILE}")")
STAGED_SOURCE_PATHS+=("${STDIN_FILE}")
HASH_SOURCE_PATHS+=("${STDIN_FILE}")
iree_debug "Wrote stdin to ${STDIN_FILE}"
fi
if [[ ${#SOURCE_FILES[@]} -gt 0 ]]; then
HAS_FILE_INPUT=1
fi
for src in "${SOURCE_FILES[@]}"; do
if [[ ! -f "${src}" ]]; then
iree_error "Source file not found: ${src}"
exit 1
fi
# Copy to staging dir with #line for accurate diagnostics.
staged_path="${STAGING_DIR}/$(basename "${src}")"
escaped_src="${src//\\/\\\\}"
escaped_src="${escaped_src//\"/\\\"}"
{
printf '#line 1 "%s"\n' "${escaped_src}"
cat "${src}"
} > "${staged_path}"
BUILD_SRCS+=("$(basename "${src}")")
STAGED_SOURCE_PATHS+=("${staged_path}")
HASH_SOURCE_PATHS+=("${src}")
iree_debug "Staged ${src} -> ${staged_path}"
done
# Determine if this is C++.
IS_CPP=0
if [[ "${LANGUAGE}" == "c++" ]]; then
IS_CPP=1
else
for src in "${BUILD_SRCS[@]}"; do
case "${src}" in
*.cc|*.cpp|*.cxx|*.C)
IS_CPP=1
break
;;
esac
done
fi
# Compute cache hash BEFORE deps inference.
# Hash is based on #includes + explicit deps + build options + language.
# File inputs also include content hash to avoid stale builds.
# This allows runs with same deps config to share BUILD.bazel analysis cache.
CACHE_HASH=$(compute_cache_hash HASH_SOURCE_PATHS EXTRA_DEPS COPTS LINKOPTS "${LANGUAGE}" BAZEL_USER_FLAGS "${HAS_FILE_INPUT}")
SLOT_DIR="${CACHE_DIR}/${CACHE_HASH}"
LOCK_FILE="${CACHE_DIR}/${CACHE_HASH}.lock"
# Ensure cache directory exists.
mkdir -p "${CACHE_DIR}"
# Acquire exclusive lock on this slot to prevent parallel runs from clobbering.
# Uses portable mkdir-based locking (flock is Linux-specific).
if ! _acquire_lock "${LOCK_FILE}" 60; then
iree_error "Timeout waiting for lock on cache slot ${CACHE_HASH}"
exit 1
fi
trap "_release_lock '${LOCK_FILE}'; cleanup_staging" EXIT
iree_debug "Acquired lock on slot ${CACHE_HASH}"
# Check cache AFTER acquiring lock (another process may have created it while we waited).
CACHE_HIT=0
if [[ -d "${SLOT_DIR}" ]] && [[ -f "${SLOT_DIR}/BUILD.bazel" ]]; then
CACHE_HIT=1
iree_debug "Cache hit: reusing slot ${CACHE_HASH} (skipping deps inference)"
fi
# Only infer deps on cache miss.
declare -A DEPS_MAP=()
declare -a DEPS=()
if [[ "${CACHE_HIT}" == "0" ]]; then
# Infer deps from source files (expensive bazel query).
declare -a INFERRED_DEPS=()
if [[ "${INFER_DEPS}" == "1" ]]; then
while IFS= read -r dep; do
if [[ -n "${dep}" ]]; then
INFERRED_DEPS+=("${dep}")
fi
done < <(infer_deps_from_sources "${STAGED_SOURCE_PATHS[@]}")
if [[ ${#INFERRED_DEPS[@]} -gt 0 ]]; then
iree_debug "Inferred deps: ${INFERRED_DEPS[*]}"
fi
fi
# Build deps list (inferred + explicit, deduplicated).
for dep in "${INFERRED_DEPS[@]}"; do
if [[ -z "${DEPS_MAP[${dep}]:-}" ]]; then
DEPS_MAP[${dep}]=1
DEPS+=("${dep}")
fi
done
for dep in "${EXTRA_DEPS[@]}"; do
if [[ -z "${DEPS_MAP[${dep}]:-}" ]]; then
DEPS_MAP[${dep}]=1
DEPS+=("${dep}")
fi
done
# If no deps at all, add base as minimum.
if [[ ${#DEPS[@]} -eq 0 ]]; then
DEPS+=("//runtime/src/iree/base")
fi
fi
# Function to generate BUILD.bazel file.
generate_build_file() {
local build_file="${1}"
shift
local -a deps=("$@")
{
echo '# Auto-generated by iree-bazel-try'
echo ''
echo 'cc_binary('
echo ' name = "snippet",'
echo ' testonly = True,'
printf ' srcs = ['
local first=1
for src in "${BUILD_SRCS[@]}"; do
if [[ "${first}" == "1" ]]; then
first=0
else
printf ', '
fi
printf '"%s"' "${src}"
done
printf '],\n'
printf ' deps = [\n'
for dep in "${deps[@]}"; do
printf ' "%s",\n' "${dep}"
done
printf ' ],\n'
echo ')'
} > "${build_file}"
}
# Set up cache slot.
if [[ "${CACHE_HIT}" == "0" ]]; then
iree_debug "Cache miss: creating slot ${CACHE_HASH}"
mkdir -p "${SLOT_DIR}"
# Generate BUILD.bazel for new slot.
BUILD_FILE="${SLOT_DIR}/BUILD.bazel"
generate_build_file "${BUILD_FILE}" "${DEPS[@]}"
iree_debug "Generated BUILD.bazel:"
if [[ "${IREE_BAZEL_VERBOSE}" == "1" ]]; then
cat "${BUILD_FILE}" | sed 's/^/ /' >&2
fi
fi
# Copy source files from staging to cache slot.
for staged_file in "${STAGED_SOURCE_PATHS[@]}"; do
cp "${staged_file}" "${SLOT_DIR}/"
done
# Touch the slot directory to update mtime for LRU tracking.
touch "${SLOT_DIR}"
# Enforce cache size limit (evict old slots if needed).
enforce_cache_limit "${CACHE_DIR}"
if [[ "${KEEP_TEMP}" == "1" ]]; then
iree_info "Cache slot: ${SLOT_DIR}"
fi
# Build the target with retry logic.
TARGET="//.iree-bazel-try/cache/${CACHE_HASH}:snippet"
BUILD_LOG="${SLOT_DIR}/build.log"
do_build() {
local BAZEL_BIN
BAZEL_BIN=$(iree_get_bazel_command)
if iree_is_verbose; then
# Verbose mode: show everything, also save to log for retry logic.
"${BAZEL_BIN}" "${BAZEL_STARTUP_ARGS[@]}" build "${TARGET}" "${BAZEL_ARGS[@]}" 9>&- 2>&1 | tee "${BUILD_LOG}"
return "${PIPESTATUS[0]}"
else
# Quiet mode: capture output, only show on failure.
# Force colors since we'll display to terminal on failure.
if "${BAZEL_BIN}" "${BAZEL_STARTUP_ARGS[@]}" build --color=yes "${TARGET}" "${BAZEL_ARGS[@]}" 9>&- >"${BUILD_LOG}" 2>&1; then
return 0
else
local exit_code=$?
cat "${BUILD_LOG}" >&2
return ${exit_code}
fi
fi
}
build_with_retry() {
local attempt=1
local current_deps=("${DEPS[@]}")
while [[ ${attempt} -le ${MAX_RETRIES} ]]; do
iree_debug "Build attempt ${attempt}/${MAX_RETRIES}"
if do_build; then
return 0
fi
# Build failed - try to find missing deps.
if [[ "${INFER_DEPS}" == "0" ]]; then
# User disabled inference, don't retry.
return 1
fi
local new_deps_found=0
while IFS= read -r new_dep; do
if [[ -n "${new_dep}" ]] && [[ -z "${DEPS_MAP[${new_dep}]:-}" ]]; then
iree_warn "Adding inferred dep: ${new_dep}"
DEPS_MAP[${new_dep}]=1
current_deps+=("${new_dep}")
new_deps_found=1
fi
done < <(parse_build_errors "${BUILD_LOG}" "${IREE_BAZEL_WORKTREE_DIR}")
if [[ "${new_deps_found}" == "0" ]]; then
# No new deps to try, give up.
iree_error "Build failed and no additional deps could be inferred"
return 1
fi
# Regenerate BUILD file with new deps.
generate_build_file "${BUILD_FILE}" "${current_deps[@]}"
iree_debug "Regenerated BUILD.bazel with new deps"
attempt=$((attempt + 1))
done
iree_error "Build failed after ${MAX_RETRIES} attempts"
return 1
}
if [[ "${COMPILE_ONLY}" == "1" ]] || [[ -n "${OUTPUT_PATH}" ]]; then
# Dry run: show what would be built and exit.
if [[ "${DRY_RUN}" == "1" ]]; then
iree_info "Command: bazel build ${TARGET} ${BAZEL_ARGS[*]}"
iree_info "Deps: ${DEPS[*]}"
exit 0
fi
iree_debug "Building ${TARGET}..."
if ! build_with_retry; then
exit 1
fi
# Copy output if requested.
if [[ -n "${OUTPUT_PATH}" ]]; then
BINARY_PATH=$(iree_bazel_get_binary_path "${TARGET}" "${BAZEL_ARGS[@]}")
if [[ -n "${BINARY_PATH}" ]] && [[ -f "${BINARY_PATH}" ]]; then
cp "${BINARY_PATH}" "${OUTPUT_PATH}"
chmod +x "${OUTPUT_PATH}"
iree_info "Binary saved to: ${OUTPUT_PATH}"
else
iree_error "Could not find built binary at ${BINARY_PATH}"
exit 1
fi
fi
if [[ "${COMPILE_ONLY}" == "1" ]]; then
iree_debug "Build successful"
exit 0
fi
fi
# Run the target.
# Dry run: show what would be run and exit.
if [[ "${DRY_RUN}" == "1" ]]; then
if [[ ${#PROGRAM_ARGS[@]} -gt 0 ]]; then
iree_info "Command: bazel build ${TARGET} ${BAZEL_ARGS[*]} && <binary> ${PROGRAM_ARGS[*]}"
else
iree_info "Command: bazel build ${TARGET} ${BAZEL_ARGS[*]} && <binary>"
fi
iree_info "Deps: ${DEPS[*]}"
iree_info "Run directory: ${IREE_BAZEL_ORIG_CWD}"
exit 0
fi
iree_debug "Running ${TARGET}..."
# Build first (using retry logic).
if ! build_with_retry; then
exit 1
fi
# Get the binary path and run from original directory.
BINARY_PATH=$(iree_bazel_get_binary_path "${TARGET}" "${BAZEL_ARGS[@]}")
if [[ -z "${BINARY_PATH}" ]] || [[ ! -x "${BINARY_PATH}" ]]; then
iree_error "Could not find built binary at ${BINARY_PATH}"
exit 1
fi
iree_debug "Binary: ${BINARY_PATH}"
iree_debug "Running from: ${IREE_BAZEL_ORIG_CWD}"
cd "${IREE_BAZEL_ORIG_CWD}"
exec "${BINARY_PATH}" "${PROGRAM_ARGS[@]}"