Create starlark rule crate Bug: 528225104 Change-Id: I949721873d6fe3ad7af42b8ff98bd00c6a6a6964 Reviewed-on: https://gn-review.googlesource.com/c/gn/+/24201 Reviewed-by: Takuto Ikuta <tikuta@google.com> Reviewed-by: Philipp Wollermann <philwo@google.com> Commit-Queue: Matt Stark <msta@google.com>
diff --git a/src/gn/starlark/Cargo.lock b/src/gn/starlark/Cargo.lock index 8d80e3f..0be2db9 100644 --- a/src/gn/starlark/Cargo.lock +++ b/src/gn/starlark/Cargo.lock
@@ -1267,6 +1267,22 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] +name = "rule" +version = "0.1.0" +dependencies = [ + "allocative", + "attr", + "depset", + "providers", + "starlark", + "starlark_derive", + "strum", + "testutils", + "thiserror", + "types", +] + +[[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/src/gn/starlark/Cargo.toml b/src/gn/starlark/Cargo.toml index dfc681c..abcc8e2 100644 --- a/src/gn/starlark/Cargo.toml +++ b/src/gn/starlark/Cargo.toml
@@ -19,6 +19,7 @@ "crates/ffi", "crates/loader", "crates/providers", + "crates/rule", "crates/testutils", "crates/types", ]
diff --git a/src/gn/starlark/crates/attr/Cargo.toml b/src/gn/starlark/crates/attr/Cargo.toml index 56b9433..e5a8ce5 100644 --- a/src/gn/starlark/crates/attr/Cargo.toml +++ b/src/gn/starlark/crates/attr/Cargo.toml
@@ -10,12 +10,12 @@ doctest = false [dependencies] +allocative = { workspace = true } +either = { workspace = true } starlark = { workspace = true } starlark_derive = { workspace = true } thiserror = { workspace = true } -allocative = { workspace = true } types = { path = "../types" } -either = { workspace = true } [dev-dependencies] testutils = { path = "../testutils" }
diff --git a/src/gn/starlark/crates/attr/src/ctx.rs b/src/gn/starlark/crates/attr/src/ctx.rs index 269da67..afca193 100644 --- a/src/gn/starlark/crates/attr/src/ctx.rs +++ b/src/gn/starlark/crates/attr/src/ctx.rs
@@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +use allocative::Allocative; use starlark::{ collections::SmallMap, values::{ @@ -10,7 +11,7 @@ FrozenHeap, FrozenValue, FrozenValueTyped, Heap, Value, }, }; -use types::LabelRef; +use types::{LabelRef, OutputType}; use crate::{ schema::{AllowFilesSchema, AttrKind, AttrSchema}, @@ -34,6 +35,7 @@ /// "foo": attr.label_list(...), /// "bar": attr.string(...), /// } +#[derive(Debug, Allocative)] pub struct CtxAttrSchema { attrs: SmallMap<String, AttrSchema>, attr: FrozenValueTyped<'static, FrozenRecordType>, @@ -43,8 +45,16 @@ impl CtxAttrSchema { /// Creates a new `CtxAttrSchema`. - pub fn new(attrs: SmallMap<String, AttrSchema>, heap: &FrozenHeap) -> Self { - let mut attrs_fields = SmallMap::with_capacity(attrs.len()); + pub fn new( + attrs: SmallMap<String, AttrSchema>, + builtin: Option<OutputType>, + heap: &FrozenHeap, + ) -> Self { + let (builtin_files, builtin_attrs) = + builtin.map(|b| b.attrs()).unwrap_or_default(); + let mut attrs_fields = SmallMap::with_capacity( + attrs.len() + builtin_files.len() + builtin_attrs.len(), + ); let mut file_fields = SmallMap::new(); let mut files_fields = SmallMap::new(); @@ -52,6 +62,14 @@ FieldGen::new(starlark::values::typing::TypeCompiled::any(), None) }; + for name in builtin_files { + attrs_fields.insert(name.to_string(), any()); + files_fields.insert(name.to_string(), any()); + } + for name in builtin_attrs { + attrs_fields.insert(name.to_string(), any()); + } + for (name, attr) in &attrs { attrs_fields.insert(name.clone(), any()); @@ -93,10 +111,18 @@ fields: &[Attr], session: &S, current_toolchain: &LabelRef, + builtin: Option<OutputType>, + mut ctx_attr: Vec<Value<'v>>, heap: &Heap<'v>, ) -> starlark::Result<CtxAttr<'v>> { - let mut ctx_attr = Vec::with_capacity(self.attr.len()); + let (builtin_files, builtin_attrs) = + builtin.map(|b| b.attrs()).unwrap_or_default(); + debug_assert!(builtin_files.len() + builtin_attrs.len() == ctx_attr.len()); + ctx_attr.reserve_exact(self.attr.len() - ctx_attr.len()); let mut ctx_files = Vec::with_capacity(self.files.len()); + for &attr_val in ctx_attr.iter().take(builtin_files.len()) { + ctx_files.push(attr_val); + } let mut ctx_file = Vec::with_capacity(self.file.len()); debug_assert!(fields.len() == self.attrs.len()); @@ -134,6 +160,10 @@ )), }) } + + pub fn attrs(&self) -> &SmallMap<String, AttrSchema> { + &self.attrs + } } #[cfg(test)] @@ -183,12 +213,15 @@ let ctx = CtxAttrSchema::new( schema.into_iter().map(|(k, v)| (k, (*v).clone())).collect(), + None, eval.frozen_heap(), ) .create_ctx_fields( &fields, &context.session, &context.current_toolchain.as_ref(), + None, + Vec::new(), &eval.heap(), )?;
diff --git a/src/gn/starlark/crates/attr/src/errors.rs b/src/gn/starlark/crates/attr/src/errors.rs index f6f2e8e..9c60497 100644 --- a/src/gn/starlark/crates/attr/src/errors.rs +++ b/src/gn/starlark/crates/attr/src/errors.rs
@@ -9,7 +9,7 @@ /// Errors returned by target attribute validation and coercion. #[derive(thiserror::Error, Debug, Clone)] -pub enum Error { +pub(crate) enum Error { #[error("value is mandatory")] MandatoryAttribute, #[error("value cannot be empty")] @@ -40,6 +40,10 @@ "target `{target}` does not produce any outputs matching allowed extensions: {allowed:?}" )] NoMatchingOutputs { target: Label, allowed: Vec<String> }, + #[error("attribute {0}: only attr.label and attr.label_list types may be overridden")] + OnlyLabelTypesMayBeOverridden(String), + #[error("attribute {0}: types of parent and child's attributes mismatch")] + AttributeTypeMismatch(String), } impl From<Error> for starlark::Error {
diff --git a/src/gn/starlark/crates/attr/src/schema.rs b/src/gn/starlark/crates/attr/src/schema.rs index 642f99e..ab51e76 100644 --- a/src/gn/starlark/crates/attr/src/schema.rs +++ b/src/gn/starlark/crates/attr/src/schema.rs
@@ -9,10 +9,11 @@ use allocative::Allocative; use starlark::{ + eval::ParametersSpecParam, starlark_simple_value, values::{ - none::NoneOr, Freeze, FreezeResult, Freezer, Heap, ProvidesStaticType, StarlarkValue, - Trace, Value, + none::NoneOr, Freeze, FreezeResult, Freezer, FrozenValue, Heap, ProvidesStaticType, + StarlarkValue, Trace, Value, }, }; use starlark_derive::{starlark_value, NoSerialize}; @@ -184,9 +185,19 @@ Ok(heap.alloc(schema)) } - /// Returns the allowed files schema for this attribute. - pub fn allow_files(&self) -> &AllowFilesSchema { - &self.allow_files + /// Verifies if a child attribute schema can override this attribute schema. + pub fn check_override(&self, name: &str, child: &Self) -> starlark::Result<()> { + if !matches!(self.kind, AttrKind::Label | AttrKind::LabelList) { + return Err(crate::Error::OnlyLabelTypesMayBeOverridden(name.to_owned()).into()); + } + if self.kind != child.kind + || self.disallow_empty != child.disallow_empty + || self.allow_files != child.allow_files + || self.cfg != child.cfg + { + return Err(crate::Error::AttributeTypeMismatch(name.to_owned()).into()); + } + Ok(()) } /// Returns the default value of this attribute, if any. @@ -202,6 +213,16 @@ AllowFilesSchema::None => None, } } + + // Converts the attribute to a parameter spec. + // Note that we intentionally do not use Defaulted(T) as it only supports + // Defaulted(starlark::Value), while we want Defaulted(Attr). + pub fn as_param_spec(&self) -> ParametersSpecParam<FrozenValue> { + match &self.default { + None => ParametersSpecParam::Required, + Some(_) => ParametersSpecParam::Optional, + } + } } #[cfg(test)]
diff --git a/src/gn/starlark/crates/rule/Cargo.toml b/src/gn/starlark/crates/rule/Cargo.toml new file mode 100644 index 0000000..bf568c7 --- /dev/null +++ b/src/gn/starlark/crates/rule/Cargo.toml
@@ -0,0 +1,21 @@ +[package] +name = "rule" +version = "0.1.0" +edition = "2021" + +[lib] +doctest = false + +[dependencies] +allocative = { workspace = true } +attr = { path = "../attr" } +depset = { path = "../depset" } +providers = { path = "../providers" } +starlark = { workspace = true } +starlark_derive = { workspace = true } +strum = { workspace = true } +thiserror = { workspace = true } +types = { path = "../types" } + +[dev-dependencies] +testutils = { path = "../testutils" }
diff --git a/src/gn/starlark/crates/rule/src/errors.rs b/src/gn/starlark/crates/rule/src/errors.rs new file mode 100644 index 0000000..7fdd277 --- /dev/null +++ b/src/gn/starlark/crates/rule/src/errors.rs
@@ -0,0 +1,26 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Errors returned by the GN Starlark rule system. +#[derive(thiserror::Error, Debug)] +pub(crate) enum Error { + #[error("Rule must be assigned to a global variable to be used")] + RuleMustBeNamed, + #[error("Parent must be a rule")] + ParentMustBeARule, + #[error("Attribute '{0}' is reserved")] + ReservedAttribute(String), +} + +impl From<Error> for starlark::Error { + fn from(err: Error) -> Self { + starlark::Error::new_other(err) + } +} + +impl From<Error> for starlark::values::FreezeError { + fn from(err: Error) -> Self { + Self::new(err.to_string()) + } +}
diff --git a/src/gn/starlark/crates/rule/src/frozen_rule.rs b/src/gn/starlark/crates/rule/src/frozen_rule.rs new file mode 100644 index 0000000..b69b980 --- /dev/null +++ b/src/gn/starlark/crates/rule/src/frozen_rule.rs
@@ -0,0 +1,324 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::{fmt, marker::PhantomData}; + +use allocative::Allocative; +use attr::{traits::EvalContextAttrExt, Attr, CtxAttrSchema}; +use starlark::{ + any::ProvidesStaticType, + collections::SmallMap, + eval::{Arguments, Evaluator, ParametersSpec}, + values::{FrozenHeap, FrozenValue, StarlarkValue, Value}, +}; +use starlark_derive::{starlark_value, NoSerialize}; +use types::{EvaluatorContextExt, Scope, TargetRef}; + +use crate::rule::{build_signature, OutputType}; + + +/// A frozen representation of a Starlark rule object. +/// +/// Once a rule has been exported from a loaded Starlark module (e.g., from +/// `.bzl` files), it is frozen into a `FrozenRule` and is ready to be invoked +/// in target build files. +#[derive(NoSerialize, Allocative)] +pub struct FrozenRule<C: EvalContextAttrExt> { + pub(crate) schema: CtxAttrSchema, + pub(crate) builtin: Option<OutputType>, + pub(crate) implementation: FrozenValue, + pub(crate) name: String, + pub(crate) signature: ParametersSpec<FrozenValue>, + pub(crate) parent: Option<&'static FrozenRule<C>>, + pub(crate) _phantom: PhantomData<C>, +} + +// Safety: FrozenRule does not automatically derive Send and Sync because of C. +// But it only contains C inside PhantomData, so Send and Sync are actually safe. +unsafe impl<C: EvalContextAttrExt> Send for FrozenRule<C> {} +unsafe impl<C: EvalContextAttrExt> Sync for FrozenRule<C> {} + +unsafe impl<'v, C: EvalContextAttrExt> ProvidesStaticType<'v> for FrozenRule<C> { + type StaticType = FrozenRule<C>; +} + +unsafe impl<'v, C: EvalContextAttrExt> starlark::values::Trace<'v> for FrozenRule<C> { + fn trace(&mut self, _tracer: &starlark::values::Tracer<'v>) {} +} + +impl<C: EvalContextAttrExt> fmt::Debug for FrozenRule<C> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FrozenRule") + .field("schema", &self.schema) + .field("builtin", &self.builtin) + .field("parent", &self.parent) + .finish() + } +} + +impl<C: EvalContextAttrExt> FrozenRule<C> { + /// Creates a new `FrozenRule` for a built-in rule. + pub fn new_builtin(builtin: OutputType, frozen_heap: &FrozenHeap) -> Self { + let schema = CtxAttrSchema::new(SmallMap::new(), Some(builtin), frozen_heap); + let name: &str = builtin.into(); + let signature = build_signature(name, &schema, true); + Self { + schema, + builtin: Some(builtin), + implementation: FrozenValue::new_none(), + name: name.to_owned(), + signature, + parent: None, + _phantom: PhantomData, + } + } +} + +#[starlark_value(type = "rule")] +impl<'v, C: EvalContextAttrExt> StarlarkValue<'v> for FrozenRule<C> +where + Self: ProvidesStaticType<'v>, +{ + type Canonical = Self; + + /// Invoking a rule generates a target. + /// Note: This is only used when calling my_rule(...) from *starlark*. + /// When calling from GN directly, we use custom logic to handle scoping + /// correctly. + fn invoke( + &self, + me: Value<'v>, + args: &Arguments<'v, '_>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + let me = me.unpack_frozen().unwrap(); + let signature = &self.signature; + + signature.parser(args, eval, |param_parser, eval| { + let target_name: &str = param_parser.next()?; + + let context = eval.context::<C>(); + let scope = context.require_macro()?; + let package = context.current_package(); + let path_resolver = context.path_resolver(); + + let attrs = self + .schema + .attrs() + .iter() + .map(|(_name, schema)| { + let value_opt: Option<Value<'v>> = param_parser.next_opt()?; + Attr::create(schema, value_opt, package, path_resolver) + }) + .collect::<Result<Vec<_>, _>>()?; + + let target = if let Some(builtin) = self.builtin { + // Collect all the arguments we don't recognise and pass them to the native + // implementation. + let kwargs: SmallMap<String, Value<'v>> = param_parser.next()?; + let child_scope = scope.copy_with(kwargs.iter().map(|(k, v)| (k.as_str(), *v))); + context.create_target(Some(builtin), target_name, &child_scope, me, attrs)? + } else { + context.create_target(None, target_name, scope, me, attrs)? + }; + target.register_dependencies(context.session(), context.current_toolchain()); + + Ok(Value::new_none()) + }) + } +} + +impl<C: EvalContextAttrExt> starlark::values::AllocFrozenValue for FrozenRule<C> { + #[inline] + fn alloc_frozen_value( + self, + heap: &starlark::values::FrozenHeap, + ) -> starlark::values::FrozenValue { + heap.alloc_simple(self) + } +} + +impl<C: EvalContextAttrExt> fmt::Display for FrozenRule<C> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "<rule: {}>", &self.name) + } +} + +#[cfg(test)] +mod tests { + use std::{ + collections::{HashMap, HashSet}, + sync::Mutex, + }; + + use attr::{Attr, LabelOrFile}; + use starlark::environment::FrozenModule; + use testutils::FakeTarget; + use types::{Label, OutputType, PackageRef}; + + use crate::globals::tests::new_assert; + + #[test] + fn test_pure_rule_inheritance() { + let mut assert = new_assert(); + let native = assert.load_module("//rules:native.scl"); + let pure = assert.load_module("//rules:pure.scl"); + + let rule = |module: &FrozenModule, name: &str| { + module.get(name).unwrap().value().unpack_frozen().unwrap() + }; + + assert.pass( + r#" +load("//rules:pure.scl", "child_rule", "parent_rule") +load("//rules:native.scl", "custom_shared_library", "static_library") + +custom_shared_library( + name = "shared_library", + mandatory = "mandatory_val", + optional = "optional_val", + unknown = "unknown", +) + +static_library( + name = "static_library", + optional = "optional_val", + unknown = "unknown" +) + +parent_rule( + name = "parent_defaulted", + parent_only = "p", +) + +child_rule( + name = "child_defaulted", + parent_only = "p", + child_only = "c", +) + +child_rule( + name = "child_override", + parent_only = "parent_val", + child_only = "child_val", + override = "//:custom_val", +) +"#, + ); + + let heap = starlark::values::FrozenHeap::new(); + let mut unknown_attrs = HashMap::new(); + unknown_attrs.insert( + "unknown".to_owned(), + starlark::values::Value::new_frozen(heap.alloc("unknown")), + ); + + let context = assert.context(); + let targets_lock = context.session.targets.lock().unwrap(); + let load = |name: &str| { + let label = Label::new(PackageRef::root().to_owned(), name.to_owned()); + targets_lock + .get(&(label, context.session.default_toolchain.clone())) + .unwrap() + .get() + }; + + assert_eq!( + *load("shared_library"), + FakeTarget { + outputs: vec![], + attrs: vec![ + Attr::String("optional_val".to_owned()), + Attr::String("mandatory_val".to_owned()), + ], + output_type: Some(OutputType::SharedLibrary), + rule: rule(&native, "custom_shared_library"), + cxx_attrs: unknown_attrs.clone(), + dependencies: Mutex::new(HashSet::new()), + } + ); + + assert_eq!( + *load("static_library"), + FakeTarget { + outputs: vec![], + attrs: vec![Attr::String("optional_val".to_owned())], + output_type: Some(OutputType::StaticLibrary), + rule: rule(&native, "static_library"), + cxx_attrs: unknown_attrs.clone(), + dependencies: Mutex::new(HashSet::new()), + } + ); + + let toolchain = Label::new( + PackageRef::root().to_owned(), + "default_toolchain".to_owned(), + ); + + assert_eq!( + *load("parent_defaulted"), + FakeTarget { + outputs: vec![], + attrs: vec![ + Attr::String("p".to_owned()), + Attr::Label(Some(LabelOrFile::Label(Label::new( + PackageRef::root().to_owned(), + "parent".to_owned() + )))), + ], + output_type: None, + rule: rule(&pure, "parent_rule"), + cxx_attrs: HashMap::new(), + dependencies: Mutex::new(HashSet::from([( + Label::new(PackageRef::root().to_owned(), "parent".to_owned()), + toolchain.clone(), + )])), + } + ); + + assert_eq!( + *load("child_defaulted"), + FakeTarget { + outputs: vec![], + attrs: vec![ + Attr::String("p".to_owned()), + Attr::Label(Some(LabelOrFile::Label(Label::new( + PackageRef::root().to_owned(), + "child".to_owned() + )))), + Attr::String("c".to_owned()), + ], + output_type: None, + rule: rule(&pure, "child_rule"), + cxx_attrs: HashMap::new(), + dependencies: Mutex::new(HashSet::from([( + Label::new(PackageRef::root().to_owned(), "child".to_owned()), + toolchain.clone(), + )])), + } + ); + + assert_eq!( + *load("child_override"), + FakeTarget { + outputs: vec![], + attrs: vec![ + Attr::String("parent_val".to_owned()), + Attr::Label(Some(LabelOrFile::Label(Label::new( + PackageRef::root().to_owned(), + "custom_val".to_owned() + )))), + Attr::String("child_val".to_owned()), + ], + output_type: None, + rule: rule(&pure, "child_rule"), + cxx_attrs: HashMap::new(), + dependencies: Mutex::new(HashSet::from([( + Label::new(PackageRef::root().to_owned(), "custom_val".to_owned()), + toolchain.clone(), + )])), + } + ); + } +}
diff --git a/src/gn/starlark/crates/rule/src/globals.rs b/src/gn/starlark/crates/rule/src/globals.rs new file mode 100644 index 0000000..463e032 --- /dev/null +++ b/src/gn/starlark/crates/rule/src/globals.rs
@@ -0,0 +1,154 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use attr::traits::EvalContextAttrExt; +use starlark::{ + environment::{FrozenModule, Module}, + values::FrozenHeapName, +}; +use strum::IntoEnumIterator; + +use crate::{FrozenRule, OutputType}; + +/// Registers and returns the built-in target rules as a frozen module. +/// Usage: +/// load("//builtins:rules.scl", "static_library") +/// +/// static_library( +/// name = "foo", +/// sources = [...], +/// ... +/// ) +pub fn register_builtin_rules<C: EvalContextAttrExt>() -> FrozenModule { + Module::with_temp_heap(|module| { + for output_type in OutputType::iter() { + let name = output_type.to_string(); + let frozen_rule = FrozenRule::<C>::new_builtin(output_type, module.frozen_heap()); + let frozen_value = module.frozen_heap().alloc(frozen_rule); + module.set(&name, frozen_value.to_value()); + } + + module + .freeze_named(FrozenHeapName::User(Box::new( + "//builtins:rules.scl".to_owned(), + ))) + .unwrap() + }) +} + +/// Registers standard target rules in the session. +#[macro_export] +macro_rules! register_rule_globals { + ($builder:expr, $ctx_type:ty) => { + #[starlark_derive::starlark_module] + fn register_rule_globals(builder: &mut starlark::environment::GlobalsBuilder) { + fn rule<'v>( + #[starlark(require = named)] implementation: starlark::values::Value<'v>, + #[starlark(require = named)] parent: Option<starlark::values::Value<'v>>, + #[starlark(require = named)] attrs: Option< + starlark::collections::SmallMap<&str, &attr::AttrSchema>, + >, + eval: &mut starlark::eval::Evaluator<'v, '_, '_>, + ) -> starlark::Result<starlark::values::Value<'v>> { + // Rules can only be created while loading scl files, not in macros or rule + // implementations. + use types::{EvalContext, EvaluatorContextExt}; + eval.context::<$ctx_type>().require_bzl()?; + + let attrs = attrs.unwrap_or_default(); + let attrs = attrs + .into_iter() + .map(|(k, v)| (k.to_string(), v.clone())) + .collect(); + let rule = <$crate::Rule<'v, $ctx_type>>::new( + attrs, + None, + parent, + implementation, + eval.frozen_heap(), + )?; + Ok(eval.heap().alloc(rule)) + } + } + register_rule_globals($builder); + }; +} + +#[cfg(test)] +pub(crate) mod tests { + use starlark::values::list::UnpackList; + use testutils::FakeEvalContext; + + pub(crate) fn make_attr_schema<'v>( + kind: attr::AttrKind, + args: attr::AttrSpecArgs<'v>, + eval: &mut starlark::eval::Evaluator<'v, '_, '_>, + ) -> starlark::Result<starlark::values::Value<'v>> { + attr::AttrSchema::create( + kind, + args, + types::PackageRef::root(), + &types::PathResolver::new_for_testing(), + &eval.heap(), + ) + } + + pub(crate) fn new_assert() -> testutils::Assert { + let mut assert = testutils::Assert::default(); + assert.modify_globals(|builder| { + crate::register_rule_globals!(builder, FakeEvalContext); + builder.set("attr", attr::AttrModule { make_attr_schema }); + }); + let builtins = crate::register_builtin_rules::<FakeEvalContext>(); + assert.module_add(builtins); + assert + } + + #[test] + fn test_rule_creation() { + let mut a = new_assert(); + let rule_value = a.pass( + r#" +my_rule = rule(implementation = None) +my_rule +"#, + ); + let my_rule = rule_value.value().unpack_frozen().unwrap(); + + a.modify_globals(move |b| { + b.set("my_rule", my_rule); + }); + + a.eq( + r#" +my_rule2 = rule(implementation = None) +[str(my_rule), str(my_rule2), str(rule(implementation = None))] +"#, + UnpackList { + items: vec![ + "<rule: my_rule>".to_owned(), + "<rule: my_rule2>".to_owned(), + "<anonymous rule>".to_owned(), + ], + }, + ); + + a.fail_to_freeze( + r#" +x = [rule(implementation = None)] +"#, + "Rule must be assigned to a global variable to be used", + ); + + a.fail( + "rule()", + "Missing named-only parameter `implementation` for call to `rule`", + ); + + a.fail( + r#"rule(implementation = lambda ctx: None, attrs = {"name": attr.string()})"#, + "Attribute 'name' is reserved", + ); + } +}
diff --git a/src/gn/starlark/crates/rule/src/lib.rs b/src/gn/starlark/crates/rule/src/lib.rs new file mode 100644 index 0000000..52fb090 --- /dev/null +++ b/src/gn/starlark/crates/rule/src/lib.rs
@@ -0,0 +1,10 @@ +pub mod errors; +pub mod frozen_rule; +pub mod globals; +pub mod rule; + +pub use attr::AttrSchema; +pub(crate) use errors::Error; +pub use frozen_rule::FrozenRule; +pub use globals::register_builtin_rules; +pub use rule::{OutputType, Rule};
diff --git a/src/gn/starlark/crates/rule/src/rule.rs b/src/gn/starlark/crates/rule/src/rule.rs new file mode 100644 index 0000000..4fba527 --- /dev/null +++ b/src/gn/starlark/crates/rule/src/rule.rs
@@ -0,0 +1,250 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::{cell::OnceCell, fmt, marker::PhantomData}; + +use allocative::Allocative; +use attr::{traits::EvalContextAttrExt, AttrSchema, CtxAttrSchema}; +use starlark::{ + any::ProvidesStaticType, + collections::SmallMap, + eval::{Arguments, Evaluator, ParametersSpec, ParametersSpecParam}, + values::{ + Freeze, FreezeResult, Freezer, FrozenHeap, FrozenValue, FrozenValueTyped, StarlarkValue, + Value, ValueLike, + }, +}; +use starlark_derive::{starlark_value, NoSerialize}; +pub use types::OutputType; +use types::EvaluatorContextExt; + +use crate::frozen_rule::FrozenRule; + +/// Representation of a Starlark rule object. +/// +/// Rules represent target definitions in GN (e.g., `executable`, `source_set`, +/// or custom rules declared via `rule()`). +/// +/// Note that a rule is unusable - it must be frozen before being used. +/// +/// The generic parameter `C` represents the execution context. This is required +/// because although we don't store it in the rule object itself, `invoke` +/// requires a concrete execution context. +#[derive(NoSerialize, Allocative)] +pub struct Rule<'v, C: EvalContextAttrExt> { + schema: CtxAttrSchema, + // Filled after `export_as` is called. + // Contains the name of the rule, and the signature required to call it. + once_named: OnceCell<(String, ParametersSpec<FrozenValue>)>, + builtin: Option<OutputType>, + implementation: Value<'v>, + parent: Option<Value<'v>>, + _phantom: PhantomData<C>, +} + +// Safety: Rule does not automatically derive Send and Sync because of C. +// But it only contains C inside PhantomData, so Send and Sync are actually safe. +unsafe impl<'v, C: EvalContextAttrExt> Send for Rule<'v, C> {} +unsafe impl<'v, C: EvalContextAttrExt> Sync for Rule<'v, C> {} + +unsafe impl<'v, C: EvalContextAttrExt> ProvidesStaticType<'v> for Rule<'v, C> { + type StaticType = Rule<'static, C>; +} + +unsafe impl<'v, C: EvalContextAttrExt> starlark::values::Trace<'v> for Rule<'v, C> { + fn trace(&mut self, tracer: &starlark::values::Tracer<'v>) { + self.implementation.trace(tracer); + self.parent.trace(tracer); + } +} + +impl<'v, C: EvalContextAttrExt> fmt::Debug for Rule<'v, C> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +/// Some reserved attributes are currently in use (eg. name). +/// Others are reserved for future use, so we disallow them in case we want to +/// support them in the future. +const RESERVED_ATTRS: &[&str] = &[ + "deps", + "name", + "public", + "public_deps", + "sources", + "testonly", + "visibility", +]; + +fn merge_attrs( + parent_attrs: &SmallMap<String, AttrSchema>, + mut child_attrs: SmallMap<String, AttrSchema>, +) -> Result<SmallMap<String, AttrSchema>, starlark::Error> { + let mut merged = SmallMap::with_capacity(parent_attrs.len() + child_attrs.len()); + + // Order matters. Parent attributes need to be added first. This allows the + // static_library builtin, for example, to declare attrs = `{"public": ..., + // "sources": ...}`, then it can simply set `values = [target.public(), + // target.sources(), starlark parameters]` + for (name, parent_schema) in parent_attrs { + if let Some(child_schema) = child_attrs.shift_remove(name) { + parent_schema.check_override(name, &child_schema)?; + merged.insert(name.clone(), child_schema); + } else { + merged.insert(name.clone(), parent_schema.clone()); + } + } + + for (name, child_schema) in child_attrs { + merged.insert(name, child_schema); + } + + Ok(merged) +} + +impl<'v, C: EvalContextAttrExt> Rule<'v, C> { + /// Creates a new `Rule` with the given attributes, implementation, and + /// parent. + pub fn new( + attrs: SmallMap<String, AttrSchema>, + builtin: Option<OutputType>, + parent: Option<Value<'v>>, + implementation: Value<'v>, + frozen_heap: &FrozenHeap, + ) -> Result<Self, starlark::Error> { + for name in attrs.keys() { + if RESERVED_ATTRS.contains(&name.as_str()) { + return Err(crate::Error::ReservedAttribute(name.clone()).into()); + } + } + + let mut builtin = builtin; + let parent_schema = match parent { + Some(parent_val) => { + if let Some(rule) = parent_val.downcast_ref::<FrozenRule<C>>() { + if builtin.is_none() { + builtin = rule.builtin; + } + Some(&rule.schema) + } else if let Some(rule) = parent_val.downcast_ref::<Rule<'v, C>>() { + if builtin.is_none() { + builtin = rule.builtin; + } + Some(&rule.schema) + } else { + return Err(crate::Error::ParentMustBeARule.into()); + } + }, + None => None, + }; + + let attrs = match parent_schema { + Some(parent_schema) => merge_attrs(parent_schema.attrs(), attrs)?, + None => attrs, + }; + + let schema = CtxAttrSchema::new(attrs, builtin, frozen_heap); + Ok(Self { + schema, + once_named: Default::default(), + builtin, + implementation, + parent, + _phantom: PhantomData, + }) + } +} + +pub(crate) fn build_signature( + name: &str, + schema: &CtxAttrSchema, + is_builtin: bool, +) -> ParametersSpec<FrozenValue> { + let named_only: Vec<_> = std::iter::once(("name", ParametersSpecParam::Required)) + .chain( + schema + .attrs() + .iter() + .map(|(k, attr)| (k.as_str(), attr.as_param_spec())), + ) + .collect(); + + ParametersSpec::new_parts(name, vec![], vec![], false, named_only, is_builtin) +} + +#[starlark_value(type = "rule")] +impl<'v, C: EvalContextAttrExt> StarlarkValue<'v> for Rule<'v, C> +where + Self: ProvidesStaticType<'v>, +{ + type Canonical = FrozenRule<C>; + + fn export_as( + &self, + variable_name: &str, + _eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<()> { + let signature = build_signature(variable_name, &self.schema, self.builtin.is_some()); + let _ = self.once_named.set((variable_name.to_owned(), signature)); + Ok(()) + } + + fn invoke( + &self, + _me: Value<'v>, + _args: &Arguments<'v, '_>, + eval: &mut Evaluator<'v, '_, '_>, + ) -> starlark::Result<Value<'v>> { + eval.context::<C>().require_macro()?; + // Rules can only be created in bzl files, so if we are in a macro, the rule + // must already be frozen. In that case, we would be calling + // FrozenRule::invoke instead. + unreachable!(); + } +} + +impl<'v, C: EvalContextAttrExt> Freeze for Rule<'v, C> { + type Frozen = FrozenRule<C>; + + fn freeze(self, freezer: &Freezer) -> FreezeResult<Self::Frozen> { + let (name, signature) = self + .once_named + .into_inner() + .ok_or(crate::Error::RuleMustBeNamed)?; + Ok(FrozenRule { + schema: self.schema, + builtin: self.builtin, + implementation: self.implementation.freeze(freezer)?, + name, + signature, + parent: match self.parent { + Some(p) => { + let frozen_parent = p.freeze(freezer)?; + let typed = FrozenValueTyped::<FrozenRule<C>>::new(frozen_parent).unwrap(); + Some(typed.as_ref()) + }, + None => None, + }, + _phantom: PhantomData, + }) + } +} + +impl<'v, C: EvalContextAttrExt> starlark::values::AllocValue<'v> for Rule<'v, C> { + #[inline] + fn alloc_value(self, heap: starlark::values::Heap<'v>) -> starlark::values::Value<'v> { + heap.alloc_complex(self) + } +} + +impl<'v, C: EvalContextAttrExt> fmt::Display for Rule<'v, C> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some((name, _)) = self.once_named.get() { + write!(f, "<rule: {name}>") + } else { + write!(f, "<anonymous rule>") + } + } +}
diff --git a/src/gn/starlark/crates/testutils/src/assert.rs b/src/gn/starlark/crates/testutils/src/assert.rs index d10726a..3239ade 100644 --- a/src/gn/starlark/crates/testutils/src/assert.rs +++ b/src/gn/starlark/crates/testutils/src/assert.rs
@@ -2,8 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -use starlark::{environment::GlobalsBuilder, values::UnpackValue}; -use types::{EvaluatorContextExt, UnpackedOwnedValue}; +use starlark::{ + environment::{FrozenModule, GlobalsBuilder}, + values::UnpackValue, +}; +use types::{EvaluatorContextExt, Label, PathResolver, UnpackedOwnedValue}; use crate::{register_globals, FakeEvalContext}; @@ -66,6 +69,23 @@ }); } + pub fn module_add(&mut self, module: FrozenModule) { + let name = module.frozen_heap().name().unwrap().to_string(); + self.assert.module_add(&name, module); + } + + pub fn load_file(&self, label: &str) -> String { + let loader = PathResolver::new_for_testing(); + let parsed = Label::parse(label, types::PackageRef::root()).unwrap(); + let path = loader.absolute_path(parsed.package(), parsed.name()); + std::fs::read_to_string(path).unwrap() + } + + pub fn load_module(&mut self, label: &str) -> FrozenModule { + let content = self.load_file(label); + self.assert.module(label, &content) + } + /// Returns a read-only reference to the fake evaluation context. pub fn context(&self) -> &FakeEvalContext { &self.context @@ -110,8 +130,7 @@ self.assert.pass(code) } - /// Asserts that freezing the evaluated module fails with the expected - /// error. + /// Asserts that freezing the evaluated module fails with the expected error. #[track_caller] pub fn fail_to_freeze(&mut self, code: &str, expected_error: &str) -> starlark::Error { self.assert.fail_to_freeze(code, expected_error)
diff --git a/src/gn/starlark/crates/testutils/src/target.rs b/src/gn/starlark/crates/testutils/src/target.rs index 94dcb2e..544231f 100644 --- a/src/gn/starlark/crates/testutils/src/target.rs +++ b/src/gn/starlark/crates/testutils/src/target.rs
@@ -15,7 +15,7 @@ values::{FrozenValue, ProvidesStaticType, StarlarkValue, Value, ValueLike}, }; use starlark_derive::{starlark_value, NoSerialize}; -use types::{File, IPromiseToImplementStarlarkEqAndHash, Label, OutputType, TargetRef}; +use types::{File, IPromiseToImplementStarlarkEqAndHash, Label, LabelRef, OutputType, Session, TargetRef}; /// A fake target struct for testing. #[derive(Debug, Allocative, Default)] @@ -129,4 +129,14 @@ fn target_out_dir(&self, prefix: &str, suffix: &str, _separator: &str) -> String { format!("{prefix}$TOOLCHAIN/{suffix}$LABEL") } + + fn register_dependencies<S: Session<TargetRef = Self>>( + &self, + session: &S, + toolchain: LabelRef<'_>, + ) { + for attr in &self.get().attrs { + attr.register_dependencies(session, self.clone(), toolchain); + } + } }
diff --git a/src/gn/starlark/crates/types/src/output_type.rs b/src/gn/starlark/crates/types/src/output_type.rs index d3af62c..abb5f20 100644 --- a/src/gn/starlark/crates/types/src/output_type.rs +++ b/src/gn/starlark/crates/types/src/output_type.rs
@@ -19,10 +19,27 @@ SourceSet, Copy, Action, - ActionForEach, + ActionForeach, BundleData, CreateBundle, GeneratedFile, RustLibrary, RustProcMacro, } + +impl OutputType { + /// Returns (attr-and-files attributes, attr-only attributes). + /// + /// Any attributes that should fill ctx.files.* should go in the former. + /// Other attributes should go in the latter. + pub fn attrs(&self) -> (&'static [&'static str], &'static [&'static str]) { + match self { + Self::Executable + | Self::SharedLibrary + | Self::LoadableModule + | Self::StaticLibrary + | Self::SourceSet => (&["sources", "public"], &["deps", "public_deps"]), + _ => (&[], &[]), + } + } +}
diff --git a/src/gn/starlark/crates/types/src/target_ref.rs b/src/gn/starlark/crates/types/src/target_ref.rs index bdbcf40..8b6c06c 100644 --- a/src/gn/starlark/crates/types/src/target_ref.rs +++ b/src/gn/starlark/crates/types/src/target_ref.rs
@@ -4,7 +4,7 @@ use starlark::values::{AllocValue, StarlarkValue}; -use crate::File; +use crate::{File, LabelRef, Session}; /// Unfortunately while we could specify that Eq and Hash are implemented, there /// is no way to delegate starlark's equality and hash function to it @@ -32,4 +32,11 @@ label_prefix: &str, package_name_separator: &str, ) -> String; + + /// Registers target dependencies contained within this target's attributes. + fn register_dependencies<S: Session<TargetRef = Self>>( + &self, + session: &S, + toolchain: LabelRef<'_>, + ); }
diff --git a/src/gn/starlark/src/testdata/rules/native.scl b/src/gn/starlark/src/testdata/rules/native.scl new file mode 100644 index 0000000..67d5259 --- /dev/null +++ b/src/gn/starlark/src/testdata/rules/native.scl
@@ -0,0 +1,24 @@ +load("//builtins:rules.scl", "static_library", "shared_library") + +def _static_library_impl(ctx): + return [] + +static_library = rule( + implementation = _static_library_impl, + parent = static_library, + attrs = { + "optional": attr.string(), + } +) + +def _shared_library_impl(ctx): + return [] + +custom_shared_library = rule( + implementation = _shared_library_impl, + parent = shared_library, + attrs = { + "optional": attr.string(), + "mandatory": attr.string(mandatory = True) + } +) \ No newline at end of file
diff --git a/src/gn/starlark/src/testdata/rules/pure.scl b/src/gn/starlark/src/testdata/rules/pure.scl new file mode 100644 index 0000000..6be8bb6 --- /dev/null +++ b/src/gn/starlark/src/testdata/rules/pure.scl
@@ -0,0 +1,22 @@ +def _parent_impl(ctx): + return [] + +parent_rule = rule( + implementation = _parent_impl, + attrs = { + "parent_only": attr.string(mandatory = True), + "override": attr.label(default = "//:parent"), + } +) + +def _child_impl(ctx): + return [] + +child_rule = rule( + implementation = _child_impl, + parent = parent_rule, + attrs = { + "child_only": attr.string(mandatory = True), + "override": attr.label(default = "//:child"), + } +)