From 6b33e07bd71d5f73b7307b5a1880f2d5de3e0761 Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Mon, 9 Feb 2026 17:01:37 -0800 Subject: [PATCH 1/2] bxl: get output artifacts for an Action - Rename action to bxl.Action so it is findable in docs (I don't know if that was what was preventing docs generation, but it's worth a try!) - Add a method to get the output artifacts for an action in bxl. This allows selectively rebuilding the action by ensuring those outputs. I added a test, but I have no way to run those besides www.metacareers.com: https://github.com/facebook/buck2/issues/1027 This is part of making action graph debugging slightly easier, which I want to prototype by writing some rather nasty BXLs: https://github.com/facebook/buck2/issues/1217 Unfortunately I *think* there's no way to get the dependencies after the fact, since I *just* have a RegisteredAction. I believe that I can probably do something very bad with owner() then rerunning the analysis to get deps though. --- .../src/bxl/starlark_defs/nodes/action.rs | 17 +++++++++++++++-- tests/core/bxl/test_audit_data/audit.bxl | 1 + .../query/aquery/test_aquery_data/aquery.bxl | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/buck2_bxl/src/bxl/starlark_defs/nodes/action.rs b/app/buck2_bxl/src/bxl/starlark_defs/nodes/action.rs index 139dec7993eea..37110b7ead7c2 100644 --- a/app/buck2_bxl/src/bxl/starlark_defs/nodes/action.rs +++ b/app/buck2_bxl/src/bxl/starlark_defs/nodes/action.rs @@ -12,9 +12,11 @@ use std::convert::Infallible; use std::sync::Arc; use allocative::Allocative; +use buck2_artifact::artifact::artifact_type::Artifact; use buck2_build_api::actions::RegisteredAction; use buck2_build_api::actions::query::ActionQueryNode; use buck2_build_api::actions::query::OwnedActionAttr; +use buck2_build_api::interpreter::rule_defs::artifact::starlark_artifact::StarlarkArtifact; use buck2_core::deferred::base_deferred_key::BaseDeferredKey; use buck2_error::buck2_error; use buck2_interpreter::types::target_label::StarlarkConfiguredTargetLabel; @@ -49,7 +51,7 @@ pub(crate) struct StarlarkAction(pub(crate) Arc); starlark_simple_value!(StarlarkAction); -#[starlark_value(type = "action")] +#[starlark_value(type = "bxl.Action")] impl<'v> StarlarkValue<'v> for StarlarkAction { fn get_methods() -> Option<&'static Methods> { static RES: MethodsStatic = MethodsStatic::new(); @@ -67,7 +69,7 @@ impl<'a> UnpackValue<'a> for StarlarkAction { } } -/// Methods for an action. +/// Methods for an action obtained from [`bxl.AuditContext.output()`](../AuditContext#output). #[starlark_module] fn action_methods(builder: &mut MethodsBuilder) { /// Gets the owning configured target label for an action. @@ -90,6 +92,17 @@ fn action_methods(builder: &mut MethodsBuilder) { .into()), } } + + /// Gets the artifacts built by this action. + fn outputs<'v>(this: StarlarkAction) -> starlark::Result> { + Ok(this + .0 + .action() + .outputs() + .iter() + .map(|a| StarlarkArtifact::new(Artifact::from(a.dupe()))) + .collect()) + } } #[derive(Debug, Display, ProvidesStaticType, Allocative)] diff --git a/tests/core/bxl/test_audit_data/audit.bxl b/tests/core/bxl/test_audit_data/audit.bxl index c9d15ec7e363e..7fbb2020ee9a6 100644 --- a/tests/core/bxl/test_audit_data/audit.bxl +++ b/tests/core/bxl/test_audit_data/audit.bxl @@ -15,6 +15,7 @@ def _audit_output_action_exists_impl(ctx): action = ctx.audit().output(buck_out) asserts.equals(action.owner(), target.label) + asserts.equals(action.outputs()[0].basename, "with_output.txt") audit_output_action_exists = bxl_main( impl = _audit_output_action_exists_impl, diff --git a/tests/core/query/aquery/test_aquery_data/aquery.bxl b/tests/core/query/aquery/test_aquery_data/aquery.bxl index 9674d6400706a..6033afef00a02 100644 --- a/tests/core/query/aquery/test_aquery_data/aquery.bxl +++ b/tests/core/query/aquery/test_aquery_data/aquery.bxl @@ -130,7 +130,7 @@ def _impl_action_query_node(ctx): action = result[0] analysis = result[1] - _assert_eq(type(action.action()), "action") + _assert_eq(type(action.action()), "bxl.Action") _assert_eq(action.rule_type, "copy") _assert_eq(str(action.action().owner().raw_target()), "root//:test") From b9dfd04e9173e85730585690cfe342dacf71337c Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Fri, 20 Feb 2026 16:07:26 -0800 Subject: [PATCH 2/2] bxl aquery: allow passing in bxl.Action This allows for fine grained action graph debugging by letting bxl ask about individual Actions' dependencies. I chose this API because it's the smallest API surface change I could think of to improve this. One thing which Claude pointed out is that you can't have heterogeneous lists of Actions and aquery nodes. This is sorta unfortunate. Allows users to implement "What deps does my action have recorded for it" from https://github.com/facebook/buck2/issues/1217 themselves. Note that I tried running the tests I could using buck bxl but I have not gone to https://metacareers.com to be able to run the python side. Example usage: Use `ctx.audit.output()` to obtain an Action object and then ask about its dependencies. --- app/buck2_build_api/src/query/bxl.rs | 6 ++++ app/buck2_bxl/src/bxl/starlark_defs/aquery.rs | 16 +++++++++ app/buck2_query_impls/src/aquery/bxl.rs | 19 ++++++++++ tests/core/query/aquery/test_aquery.py | 13 +++++++ .../query/aquery/test_aquery_data/aquery.bxl | 35 +++++++++++++++++++ .../query/aquery/test_aquery_data/prelude.bzl | 3 +- 6 files changed, 91 insertions(+), 1 deletion(-) diff --git a/app/buck2_build_api/src/query/bxl.rs b/app/buck2_build_api/src/query/bxl.rs index dabda4d754118..30f7600b85745 100644 --- a/app/buck2_build_api/src/query/bxl.rs +++ b/app/buck2_build_api/src/query/bxl.rs @@ -12,6 +12,7 @@ use std::future::Future; use std::pin::Pin; use async_trait::async_trait; +use buck2_artifact::actions::key::ActionKey; use buck2_core::cells::CellResolver; use buck2_core::cells::name::CellName; use buck2_core::configuration::compatibility::MaybeCompatible; @@ -183,6 +184,11 @@ pub trait BxlAqueryFunctions: Send { dice: &mut DiceComputations<'_>, targets: &TargetSet, ) -> buck2_error::Result>; + async fn get_action_nodes( + &self, + dice: &mut DiceComputations<'_>, + action_keys: Vec, + ) -> buck2_error::Result>; } pub static NEW_BXL_CQUERY_FUNCTIONS: LateBinding< diff --git a/app/buck2_bxl/src/bxl/starlark_defs/aquery.rs b/app/buck2_bxl/src/bxl/starlark_defs/aquery.rs index 1fae20b093562..df1b7e558f70c 100644 --- a/app/buck2_bxl/src/bxl/starlark_defs/aquery.rs +++ b/app/buck2_bxl/src/bxl/starlark_defs/aquery.rs @@ -9,6 +9,7 @@ */ use allocative::Allocative; +use buck2_artifact::actions::key::ActionKey; use buck2_build_api::actions::query::ActionQueryNode; use buck2_build_api::query::bxl::BxlAqueryFunctions; use buck2_build_api::query::bxl::NEW_BXL_AQUERY_FUNCTIONS; @@ -46,6 +47,7 @@ use starlark::values::type_repr::StarlarkTypeRepr; use crate::bxl::starlark_defs::context::BxlContext; use crate::bxl::starlark_defs::context::ErrorPrinter; +use crate::bxl::starlark_defs::nodes::action::StarlarkAction; use crate::bxl::starlark_defs::nodes::action::StarlarkActionQueryNode; use crate::bxl::starlark_defs::providers_expr::AnyProvidersExprArg; use crate::bxl::starlark_defs::providers_expr::ProvidersExpr; @@ -122,6 +124,7 @@ enum UnpackActionNodes<'v> { ActionQueryNodesSet(&'v StarlarkTargetSet), ConfiguredProviders(AnyProvidersExprArg<'v>), ConfiguredTargets(ConfiguredTargetListExprArg<'v>), + StarlarkActions(UnpackList), } // Aquery operates on `ActionQueryNode`s. Under the hood, the target set of action query nodes is obtained @@ -140,6 +143,14 @@ async fn unpack_action_nodes<'v>( return Ok(action_nodes.into_iter().map(|v| v.0).collect()); } UnpackActionNodes::ActionQueryNodesSet(action_nodes) => return Ok(action_nodes.0.clone()), + UnpackActionNodes::StarlarkActions(actions) => { + let action_keys: Vec = actions + .into_iter() + .map(|starlark_action| starlark_action.0.key().dupe()) + .collect(); + + return aquery_env.get_action_nodes(dice, action_keys).await; + } UnpackActionNodes::ConfiguredProviders(arg) => { ProvidersExpr::::unpack( arg, @@ -183,6 +194,11 @@ async fn unpack_action_nodes<'v>( /// /// Query results are `target_set`s of `action_query_node`s, which supports iteration, /// indexing, `len()`, set addition/subtraction, and `equals()`. +/// +/// Actions can be specified as: +/// - Target expressions (configured targets/providers) +/// - Existing action query nodes or target sets +/// - `bxl.Action` objects (obtained from `ctx.audit().output()`) #[starlark_module] fn aquery_methods(builder: &mut MethodsBuilder) { /// The deps query for finding the transitive closure of dependencies. diff --git a/app/buck2_query_impls/src/aquery/bxl.rs b/app/buck2_query_impls/src/aquery/bxl.rs index 28cc4a7ac78f9..b51b3f9d32a80 100644 --- a/app/buck2_query_impls/src/aquery/bxl.rs +++ b/app/buck2_query_impls/src/aquery/bxl.rs @@ -12,6 +12,7 @@ use std::marker::PhantomData; use std::sync::Arc; use async_trait::async_trait; +use buck2_artifact::actions::key::ActionKey; use buck2_build_api::actions::query::ActionQueryNode; use buck2_build_api::analysis::calculation::RuleAnalysisCalculation; use buck2_build_api::query::bxl::BxlAqueryFunctions; @@ -303,6 +304,24 @@ impl BxlAqueryFunctions for BxlAqueryFunctionsImpl { }) .await } + + async fn get_action_nodes( + &self, + dice: &mut DiceComputations<'_>, + action_keys: Vec, + ) -> buck2_error::Result> { + dice.with_linear_recompute(|dice| async move { + let delegate = self.aquery_delegate(&dice).await?; + let mut result = TargetSet::new(); + let nodes = buck2_util::future::try_join_all( + action_keys.iter().map(|key| delegate.get_action_node(&key)), + ) + .await?; + result.extend(nodes); + Ok(result) + }) + .await + } } pub(crate) fn init_new_bxl_aquery_functions() { diff --git a/tests/core/query/aquery/test_aquery.py b/tests/core/query/aquery/test_aquery.py index f8aa4842dfa45..9a46670bac0c0 100644 --- a/tests/core/query/aquery/test_aquery.py +++ b/tests/core/query/aquery/test_aquery.py @@ -154,3 +154,16 @@ async def test_bxl_aquery_eval(buck: Buck) -> None: @buck_test() async def test_bxl_aquery_action_query_node(buck: Buck) -> None: await buck.bxl("//:aquery.bxl:action_query_node") + + +# Tests for bxl.Action support in aquery operations +@buck_test() +async def test_bxl_action_deps_0(buck: Buck) -> None: + """Test passing a bxl.Action to aquery.deps() with depth=0 returns itself""" + await buck.bxl("//:aquery.bxl:action_deps_0") + + +@buck_test() +async def test_bxl_action_deps(buck: Buck) -> None: + """Test that you can isolate the deps of one action by itself""" + await buck.bxl("//:aquery.bxl:action_deps") diff --git a/tests/core/query/aquery/test_aquery_data/aquery.bxl b/tests/core/query/aquery/test_aquery_data/aquery.bxl index 6033afef00a02..b28c7b4711da4 100644 --- a/tests/core/query/aquery/test_aquery_data/aquery.bxl +++ b/tests/core/query/aquery/test_aquery_data/aquery.bxl @@ -141,3 +141,38 @@ action_query_node = bxl_main( impl = _impl_action_query_node, cli_args = {}, ) + +def _impl_action_deps_0(ctx): + """Test passing a bxl.Action to aquery.deps() returns itself""" + nodes = ctx.aquery().eval("//:test") + + action = nodes[0].action() + + # Pass the bxl.Action rather than a target to aquery.deps() + result = ctx.aquery().deps([action], depth = 0) + _assert_eq(action.outputs(), result[0].action().outputs()) + +action_deps_0 = bxl_main( + impl = _impl_action_deps_0, + cli_args = {}, +) + +def _impl_action_deps(ctx): + """Test that you can isolate the deps of one action by itself""" + nodes = ctx.aquery().eval("//:test") + + action = nodes[0].action() + + # Pass the bxl.Action rather than a target to aquery.deps() + result = ctx.aquery().deps([action], depth = 1) + + _assert_eq(len(result), 2) + # First should be itself + _assert_eq(action.outputs(), result[0].action().outputs()) + dep = result[1] + _assert_eq(dep.action().outputs()[0].short_path, "dep") + +action_deps = bxl_main( + impl = _impl_action_deps, + cli_args = {}, +) diff --git a/tests/core/query/aquery/test_aquery_data/prelude.bzl b/tests/core/query/aquery/test_aquery_data/prelude.bzl index d1e764fd4f8c7..6a70b76b47714 100644 --- a/tests/core/query/aquery/test_aquery_data/prelude.bzl +++ b/tests/core/query/aquery/test_aquery_data/prelude.bzl @@ -9,6 +9,7 @@ def _test(ctx: AnalysisContext): dep = ctx.actions.write("dep", "") default = ctx.actions.copy_file("default", dep) + other_default = ctx.actions.copy_file("other_default", dep) other = ctx.actions.write("other", "") sub_default = ctx.actions.write("sub_default", "") @@ -18,7 +19,7 @@ def _test(ctx: AnalysisContext): ctx.actions.write("unused", "") return [DefaultInfo( - default_outputs = [default], + default_outputs = [default, other_default], other_outputs = [other], sub_targets = { "sub": [