Skip to content
Open
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
7 changes: 7 additions & 0 deletions .aspect/config.axl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ load("@aspect//lib/gitlab_detect_test.axl", "gitlab_detect_tests")
load("@aspect//lib/gitlab_test.axl", "gitlab_client_tests")
load("@aspect//lib/lint_results_test.axl", "lint_annotation_plan_tests", "lint_template_snapshot_tests")
load("@aspect//lib/rate_limit_test.axl", "rate_limit_tests")
load("@aspect//lib/runnable_test.axl", "runnable_tests")
load("@aspect//lib/runner_job_history_test.axl", "runner_job_history_tests")
load("@aspect//lib/tips_test.axl", "tips_tests")
load("@aspect//traits.axl", "BazelTrait", "LintTrait")
Expand Down Expand Up @@ -120,6 +121,12 @@ def config(ctx: ConfigContext):
# Run with: aspect dev test-runner-job-history
ctx.tasks.add(runner_job_history_tests)

# Label canonicalizer — `canonical_label` across absolute, @repo,
# `:name`, bare, and `pkg:name` forms with and without a current
# package context.
# Run with: aspect dev test-runnable
ctx.tasks.add(runnable_tests)

# Delivery template snapshot tests — renders delivery_results templates.
# Run with: aspect dev test-delivery-template-snapshots
ctx.tasks.add(delivery_template_snapshot_tests)
Expand Down
13 changes: 13 additions & 0 deletions .buildkite/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ steps:
cd crates/aspect-cli
$$LAUNCHER user user-task-subdir
)
echo "--- :aspect: aspect run smoke (in examples/deliverable)"
# Exercises the run task end-to-end across the three label forms the
# canonicalizer resolves (:name, //:name, bare name) and verifies
# trailing args are forwarded to the spawned binary.
(
cd examples/deliverable
$$LAUNCHER run :py_deliverable
$$LAUNCHER run //:py_deliverable
$$LAUNCHER run py_deliverable
$$LAUNCHER run :deliverable -- forwarded-arg
)
echo "--- :aspect: aspect help"
$$LAUNCHER help
echo "--- :aspect: aspect-launcher --version"
Expand Down Expand Up @@ -133,6 +144,8 @@ steps:
$$LAUNCHER dev test-rate-limit
echo "--- :aspect: aspect dev test-runner-job-history"
$$LAUNCHER dev test-runner-job-history
echo "--- :aspect: aspect dev test-runnable"
$$LAUNCHER dev test-runnable
echo "--- :aspect: aspect tests axl"
# Run AXL tests last — cancel tests may kill the Bazel server,
# which invalidates bazel-out paths used by $$LAUNCHER above.
Expand Down
1 change: 1 addition & 0 deletions crates/aspect-cli/src/builtins/aspect/MODULE.aspect
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use_task("auth.axl", "logout")
use_task("auth.axl", "whoami")
use_task("build.axl", "build")
use_task("github.axl", "token")
use_task("run.axl", "run")
use_task("test.axl", "test")
use_task("axl_add.axl", "add")
use_task("delivery.axl", "delivery")
Expand Down
57 changes: 51 additions & 6 deletions crates/aspect-cli/src/builtins/aspect/lib/runnable.axl
Original file line number Diff line number Diff line change
@@ -1,9 +1,52 @@
_DEFAULT_WORKSPACE_NAME = "_main"

def canonical_label(label):
if ":" not in label:
return label + ":" + label.split("/")[-1]
return label
def canonical_label(label: str, current_package: str = "") -> str:
"""Normalize a Bazel label to `//<pkg>:<name>` (or `@<repo>//<pkg>:<name>`) form.

`current_package` is the workspace-relative path of the user's working
package (e.g. "foo/bar", or "" for the root package). Required to resolve
relative labels (`:foo`, `foo`, `pkg:foo`). Absolute labels (`//...`,
`@repo//...`) ignore it.

canonical_label("//foo/bar") -> "//foo/bar:bar"
canonical_label("//foo/bar:baz") -> "//foo/bar:baz"
canonical_label("@repo//foo") -> "@repo//foo:foo"
canonical_label(":baz", "foo/bar") -> "//foo/bar:baz"
canonical_label("baz", "foo/bar") -> "//foo/bar:baz"
canonical_label("sub:baz", "foo") -> "//foo/sub:baz"
canonical_label("baz", "") -> "//:baz"
"""
# Absolute: //pkg[:name] or @repo//pkg[:name].
if label.startswith("//") or label.startswith("@"):
pkg_part = label.split("//", 1)[-1]
if ":" in pkg_part:
return label
if pkg_part:
return label + ":" + pkg_part.rsplit("/", 1)[-1]
return label

# `:name` -> //<current_package>:name
if label.startswith(":"):
return "//" + current_package + label

# `pkg:name` -> //<current_package>/<pkg>:name (or //<pkg>:name when at root)
if ":" in label:
pkg, _, name = label.partition(":")
joined = current_package + "/" + pkg if current_package else pkg
return "//" + joined + ":" + name

# Bare `name` -> //<current_package>:name
return "//" + current_package + ":" + label

def _current_package(ctx) -> str:
"""Workspace-relative path of cwd, used as the package for relative labels."""
root = ctx.std.env.root_dir().rstrip("/")
cwd = ctx.std.env.current_dir().rstrip("/")
if cwd == root:
return ""
if cwd.startswith(root + "/"):
return cwd[len(root) + 1:]
return ""

def _process_bes(state: struct, event):
if event.kind == "named_set_of_files":
Expand Down Expand Up @@ -35,7 +78,7 @@ def _determine_entrypoint(state: struct, target: str) -> str | None:
caller pairs this with `is_runnable` to distinguish "not in BES" from
"in BES but no runfiles tree".
"""
target = canonical_label(target)
target = canonical_label(target, state.current_package)
if target not in state.target_fileset:
return None

Expand Down Expand Up @@ -98,9 +141,11 @@ def _spawn(ctx: TaskContext, state: struct, entrypoint: str, args: list[str], ca
return cmd.spawn()

def runnable(ctx, targets: list[str]) -> struct:
current_package = _current_package(ctx)
state = struct(
ctx = ctx,
targets = {canonical_label(t): t for t in targets},
current_package = current_package,
targets = {canonical_label(t, current_package): t for t in targets},
filesets = {},
target_fileset = {},
# Single-element list so `_process_bes` can mutate it (struct fields are
Expand Down
80 changes: 80 additions & 0 deletions crates/aspect-cli/src/builtins/aspect/lib/runnable_test.axl
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Tests for `lib/runnable.axl`'s label canonicalizer.

Covers `canonical_label` across:
- Absolute labels: `//pkg:name`, `//pkg` (shorthand), `//:name`, `@repo//pkg:name`, `@repo//pkg` (shorthand).
- Relative labels: `:name`, bare `name`, `pkg:name` — both at the workspace root and inside a nested package.

`_current_package` derives the package from cwd vs workspace root; that path
needs a real cwd/root pair and is exercised by the live `aspect run` smoke
in CI rather than mocked here.

Run with:
aspect dev test-runnable
"""

load("./runnable.axl", "canonical_label")

def _eq(label, got, want):
if got != want:
fail("%s: got %r, want %r" % (label, got, want))

def _test_absolute_unchanged_when_target_present():
_eq("//foo/bar:baz", canonical_label("//foo/bar:baz"), "//foo/bar:baz")
_eq("//:foo", canonical_label("//:foo"), "//:foo")
_eq("//foo/bar:baz with pkg context", canonical_label("//foo/bar:baz", "ignored"), "//foo/bar:baz")

def _test_absolute_shorthand_expands_basename():
_eq("//foo/bar -> //foo/bar:bar", canonical_label("//foo/bar"), "//foo/bar:bar")
_eq("//foo -> //foo:foo", canonical_label("//foo"), "//foo:foo")

def _test_external_repo_labels():
_eq("@repo//foo:bar", canonical_label("@repo//foo:bar"), "@repo//foo:bar")
_eq("@repo//foo -> @repo//foo:foo", canonical_label("@repo//foo"), "@repo//foo:foo")
_eq("@@_main//foo:bar (bzlmod canonical)", canonical_label("@@_main//foo:bar"), "@@_main//foo:bar")

def _test_relative_colon_name_at_root():
_eq(":foo at root -> //:foo", canonical_label(":foo", ""), "//:foo")

def _test_relative_colon_name_in_nested_package():
_eq(":foo in examples/deliverable", canonical_label(":foo", "examples/deliverable"), "//examples/deliverable:foo")

def _test_bare_name_at_root():
_eq("foo at root -> //:foo", canonical_label("foo", ""), "//:foo")

def _test_bare_name_in_nested_package():
_eq("foo in examples -> //examples:foo", canonical_label("foo", "examples"), "//examples:foo")

def _test_relative_pkg_colon_name_at_root():
_eq("sub:foo at root -> //sub:foo", canonical_label("sub:foo", ""), "//sub:foo")

def _test_relative_pkg_colon_name_in_nested_package():
_eq("sub:foo in foo -> //foo/sub:foo", canonical_label("sub:foo", "foo"), "//foo/sub:foo")

def _test_default_current_package_is_root():
"""Callers that omit `current_package` (e.g. `_process_bes` on BES events)
get root-package resolution, which is correct because BES always emits
absolute labels — so the default never actually engages on a relative
input in practice."""
_eq(":foo with default pkg", canonical_label(":foo"), "//:foo")
_eq("foo with default pkg", canonical_label("foo"), "//:foo")

def _test_impl(ctx):
_test_absolute_unchanged_when_target_present()
_test_absolute_shorthand_expands_basename()
_test_external_repo_labels()
_test_relative_colon_name_at_root()
_test_relative_colon_name_in_nested_package()
_test_bare_name_at_root()
_test_bare_name_in_nested_package()
_test_relative_pkg_colon_name_at_root()
_test_relative_pkg_colon_name_in_nested_package()
_test_default_current_package_is_root()
print("runnable.axl: OK (10 sections)")
return 0

runnable_tests = task(
name = "test-runnable",
group = ["dev"],
implementation = _test_impl,
args = {},
)
144 changes: 144 additions & 0 deletions crates/aspect-cli/src/builtins/aspect/run.axl
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""A default 'run' task that builds a target with `bazel build` and runs the resulting binary."""

load("@std//time.axl", "sleep_iter")
load("./bazel.axl", "BazelTrait")
load("./lib/bazel_results.axl", "BES_DRAIN_TICK_MS", "init_data", "process_event")
load("./lib/environment.axl", "error")
load("./lib/health_check.axl", "HealthCheckTrait")
load("./lib/lifecycle.axl", "Phase", "ProgressStyles", "TaskLifecycleTrait", "preflight_phase", "setup_phase", "task_update")
load("./lib/runnable.axl", "runnable")
load("./lib/tips.axl", "tips")

def _impl(ctx: TaskContext) -> int:
bazel_trait = ctx.traits[BazelTrait]
hc_trait = ctx.traits[HealthCheckTrait]
lifecycle = ctx.traits[TaskLifecycleTrait]

target = ctx.args.target[0]
forward_args = list(ctx.args.run_args)

data = init_data()
setup_phase(ctx, lifecycle)
for handler in lifecycle.task_started:
handler(ctx, target)

preflight_phase(ctx, lifecycle, hc_trait)

for hook in hc_trait.post_health_check:
result = hook(ctx)
if result != None:
fail(result)

flags = []
flags.extend(bazel_trait.extra_flags)
for hook in bazel_trait.task_flags:
flags.extend(hook(ctx))
if bazel_trait.flags:
flags = bazel_trait.flags(flags)

startup_flags = list(bazel_trait.extra_startup_flags)
if bazel_trait.startup_flags:
startup_flags = bazel_trait.startup_flags(startup_flags)

rc = ctx.bazel.parse_rc(
root = ctx.std.env.root_dir(),
flags = flags,
startup_flags = startup_flags,
)

# `run` inherits the `build` section in Bazel's rc hierarchy, so this picks
# up `common`, `build`, and `run` flags. Forwarded as-is to ctx.bazel.build.
rc_startup_flags, flags = rc.expand_all(command = "run")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Expand RC flags for build, not run

aspect run shells out with ctx.bazel.build(...), but this code expands .bazelrc as command = "run", which injects run-only flags into a build invocation. Bazel’s command-line reference lists flags like --script_path under Run Options (not build), so users with run entries in .bazelrc can hit unrecognized option failures before execution. This should expand build-compatible flags (or filter run-only ones) before calling build.

Useful? React with 👍 / 👎.

ctx.bazel.startup_flags.extend(startup_flags + rc_startup_flags + ["--ignore_all_rc_files"])

# Required for the spawn to succeed: keep BES artifact references
# local, materialize the binary on disk, and lay down the runfiles
# tree. Appended LAST so they win over any --config=X expansion
# introduced by the rc.
flags.append("--experimental_build_event_upload_strategy=local")
flags.append("--remote_download_outputs=toplevel")
flags.append("--build_runfile_links")

r = runnable(ctx, [target])
events = bazel.build_events.iterator()
build_events = [events] + list(bazel_trait.build_event_sinks)

for hook in bazel_trait.build_start:
hook(ctx)

task_update(
ctx,
lifecycle,
"running",
ProgressStyles(
body = "Building %s..." % target,
stream = "Building %s" % target,
),
kind = "bazel_results",
data = data,
phase = Phase(name = "build", description = "Build run target", emoji = "🔨"),
)

build = ctx.bazel.build(target, build_events = build_events, flags = flags)

for _tick in sleep_iter(BES_DRAIN_TICK_MS):
for _ in range(10000):
event = events.try_pop()
if event == None:
break
r.event(event)
for handler in bazel_trait.build_event:
handler(ctx, event)
process_event(data, event)
if events.done:
break

build_status = build.wait()
for sink in bazel_trait.build_event_sinks:
sink.wait()
for hook in bazel_trait.build_end:
hook(ctx, build_status.code)

if not build_status.success:
return build_status.code or 1

entrypoint = r.determine_entrypoint(target)
if not entrypoint:
error(ctx.std, "Failed to determine the run entrypoint for %s." % target)
return 1
if not r.is_runnable(entrypoint):
error(ctx.std, "%s is not runnable (no runfiles tree at %s.runfiles)." % (target, entrypoint))
return 1

task_update(
ctx,
lifecycle,
"running",
ProgressStyles(
body = "Running %s..." % target,
stream = "Running %s" % target,
),
phase = Phase(name = "run", description = "Run target", emoji = "🚀"),
)

return r.spawn(entrypoint, forward_args).wait().code

run = task(
summary = "Build a target with bazel and run the resulting binary.",
args = {
"target": args.positional(
minimum = 1,
maximum = 1,
description = "Bazel target to build and run.",
),
"run_args": args.trailing_var_args(
description = "Arguments forwarded to the target binary. Separate from `aspect run`'s own flags with `--`, e.g. `aspect run //foo:bar -- --binary-arg value`.",
),
},
traits = [
BazelTrait,
HealthCheckTrait,
TaskLifecycleTrait,
] + tips.TRAITS,
implementation = _impl,
)
3 changes: 2 additions & 1 deletion crates/aspect-cli/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,8 @@ fn arg_to_clap(scope: Scope<'_>, name: &str, arg: &Arg) -> ClapArg {
.value_parser(value_parser!(String))
.value_name(id)
.help(help_text(description))
.num_args(*minimum as usize..=*maximum as usize);
.num_args(*minimum as usize..=*maximum as usize)
.required(*minimum > 0 && default.is_none());
if let Some(default) = default {
it = it.default_values(default);
}
Expand Down
Loading