Implement more features on ctx. * ctx.label * ctx.actions.declare_file * ctx.actions.args * ctx.actions.write Change-Id: I9e2aaa33f4f3cfa3ceb7d8cfc94c11156a6a6964 Reviewed-on: https://gn-review.googlesource.com/c/gn/+/27140 Reviewed-by: Takuto Ikuta <tikuta@google.com> Commit-Queue: Matt Stark <msta@google.com>
diff --git a/integration_tests/starlark/filegroup.scl b/integration_tests/starlark/filegroup.scl index 958e9d1..80a5c5a 100644 --- a/integration_tests/starlark/filegroup.scl +++ b/integration_tests/starlark/filegroup.scl
@@ -2,13 +2,19 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -FilegroupInfo = provider(fields = ["files"]) +FilegroupInfo = provider(fields = ["files", "manifest"]) def _filegroup_impl(ctx): files = depset(ctx.files.srcs) + manifest = ctx.actions.declare_file(ctx.label.name + ".manifest") + ctx.actions.write( + manifest, + "manifest for " + str(ctx.label) + "\n" + + "\n".join(sorted([f.path for f in ctx.files.srcs])) + ) return [ DefaultInfo(files = files), - FilegroupInfo(files = files), + FilegroupInfo(files = files, manifest = manifest), ] filegroup = rule(
diff --git a/integration_tests/starlark/goldens/obj/filegroup/filegroup.manifest b/integration_tests/starlark/goldens/obj/filegroup/filegroup.manifest new file mode 100644 index 0000000..a717d42 --- /dev/null +++ b/integration_tests/starlark/goldens/obj/filegroup/filegroup.manifest
@@ -0,0 +1,2 @@ +manifest for //:filegroup +../first.txt \ No newline at end of file
diff --git a/src/gn/starlark/Cargo.lock b/src/gn/starlark/Cargo.lock index 40cb2aa..c4cba15 100644 --- a/src/gn/starlark/Cargo.lock +++ b/src/gn/starlark/Cargo.lock
@@ -788,6 +788,12 @@ ] [[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] name = "ffi" version = "0.1.0" dependencies = [ @@ -1370,6 +1376,12 @@ ] [[package]] +name = "normalize-path" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5438dd2b2ff4c6df6e1ce22d825ed2fa93ee2922235cc45186991717f0a892d" + +[[package]] name = "num-bigint" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1731,12 +1743,15 @@ version = "0.1.0" dependencies = [ "allocative", + "anyhow", + "args", "attr", "depset", "providers", "starlark", "starlark_derive", "strum", + "tempfile", "testutils", "thiserror", "types", @@ -2197,6 +2212,19 @@ checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" [[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2309,6 +2337,7 @@ dependencies = [ "allocative", "cxx", + "normalize-path", "starlark", "starlark_derive", "strum",
diff --git a/src/gn/starlark/Cargo.toml b/src/gn/starlark/Cargo.toml index 7b1e6c1..fd70dfb 100644 --- a/src/gn/starlark/Cargo.toml +++ b/src/gn/starlark/Cargo.toml
@@ -37,10 +37,12 @@ cxx = { version = "1.0", features = ["c++20"] } either = "1.18" link-cplusplus = "1.0" +normalize-path = "0.2" starlark = "0.14.2" starlark_derive = "0.14.2" thiserror = "2.0" strum = { version = "0.28", features = ["derive"] } +tempfile = "3.27" [features] # When compiling with ninja, ninja has its own set of link flags.
diff --git a/src/gn/starlark/crates/rule/Cargo.toml b/src/gn/starlark/crates/rule/Cargo.toml index bf568c7..c4cd8b0 100644 --- a/src/gn/starlark/crates/rule/Cargo.toml +++ b/src/gn/starlark/crates/rule/Cargo.toml
@@ -8,6 +8,7 @@ [dependencies] allocative = { workspace = true } +args = { path = "../args" } attr = { path = "../attr" } depset = { path = "../depset" } providers = { path = "../providers" } @@ -18,4 +19,6 @@ types = { path = "../types" } [dev-dependencies] +anyhow = { workspace = true } +tempfile = { workspace = true } testutils = { path = "../testutils" }
diff --git a/src/gn/starlark/crates/rule/src/actions.rs b/src/gn/starlark/crates/rule/src/actions.rs new file mode 100644 index 0000000..52317c0 --- /dev/null +++ b/src/gn/starlark/crates/rule/src/actions.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 std::{ + fmt::{self, Display, Formatter}, + marker::PhantomData, +}; + +use allocative::Allocative; +use attr::{traits::EvalContextAttrExt, TargetAttrExt}; +use starlark::{ + any::ProvidesStaticType, + environment::Methods, + values::{AllocValue, Freeze, FreezeResult, Freezer, Heap, StarlarkValue, Trace, Value}, +}; +use starlark_derive::{starlark_value, NoSerialize}; +use types::{CtxMethods, Session}; + +/// The `ctx.actions` object which provides APIs to declare output files and +/// register build actions. +#[derive(Allocative, NoSerialize, Trace)] +#[trace(bound = "C: EvalContextAttrExt + CtxMethods")] +pub struct Actions<C: EvalContextAttrExt + CtxMethods> { + // We don't need to store ctx because it's stored already in the EvalContext. + _marker: PhantomData<fn() -> C>, +} + +// Safety: This is a zero-sized type. All are equivalent. +unsafe impl<'v, C: EvalContextAttrExt + CtxMethods> ProvidesStaticType<'v> for Actions<C> { + type StaticType = Actions<C>; +} + +impl<C: EvalContextAttrExt + CtxMethods> Default for Actions<C> { + fn default() -> Self { + Self { + _marker: PhantomData, + } + } +} + +impl<C: EvalContextAttrExt + CtxMethods> std::fmt::Debug for Actions<C> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ctx.actions") + } +} + +impl<C: EvalContextAttrExt + CtxMethods> Display for Actions<C> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ctx.actions") + } +} + +#[starlark_value(type = "ctx.actions")] +impl<'v, C: EvalContextAttrExt + CtxMethods> StarlarkValue<'v> for Actions<C> +where + <C::Session as Session>::TargetRef: TargetAttrExt, +{ + type Canonical = Self; + + fn get_methods() -> Option<&'static Methods> { + Some(<C as CtxMethods>::actions_methods()) + } +} + +impl<'v, C: EvalContextAttrExt + CtxMethods> AllocValue<'v> for Actions<C> +where + <C::Session as Session>::TargetRef: TargetAttrExt, +{ + fn alloc_value(self, heap: Heap<'v>) -> Value<'v> { + heap.alloc_complex(self) + } +} + +impl<'v, C: EvalContextAttrExt + CtxMethods> Freeze<'v> for Actions<C> +where + <C::Session as Session>::TargetRef: TargetAttrExt, +{ + type Frozen<'fv> = starlark::values::none::NoneType; + + fn freeze<'fv>(self, _packer: &Freezer<'v, 'fv>) -> FreezeResult<Self::Frozen<'fv>> { + Err(crate::errors::Error::ObjectUnfreezable("ctx.actions").into()) + } +} + +#[macro_export] +macro_rules! impl_actions_methods { + ($ctx_type:ty) => { + #[starlark_derive::starlark_module] + pub fn actions_methods(builder: &mut starlark::environment::MethodsBuilder) { + fn declare_file<'v>( + this: starlark::values::Value<'v>, + filename: &str, + eval: &mut starlark::eval::Evaluator<'v, '_, '_>, + ) -> starlark::Result<starlark::values::Value<'v>> { + let _ = this; + use types::{EvalContext as _, EvaluatorContextExt as _}; + let file = eval + .context::<$ctx_type>() + .require_rule_impl()? + .borrow_mut() + .declare_file(filename)?; + Ok(eval.heap().alloc(file)) + } + + fn args<'v>( + this: starlark::values::Value<'v>, + eval: &mut starlark::eval::Evaluator<'v, '_, '_>, + ) -> starlark::Result<starlark::values::Value<'v>> { + let _ = this; + Ok(eval.heap().alloc($crate::args::Args::default())) + } + + fn write<'v>( + this: starlark::values::Value<'v>, + output: &types::File, + content: &str, + #[starlark(default = false)] is_executable: bool, + eval: &mut starlark::eval::Evaluator<'v, '_, '_>, + ) -> starlark::Result<starlark::values::none::NoneType> { + let _ = this; + use types::{EvalContext as _, EvaluatorContextExt as _}; + let ctx = eval.context::<$ctx_type>(); + ctx.require_rule_impl()? + .borrow_mut() + .generates_file(output)?; + + let path = ctx.path_resolver().resolve(output); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(starlark::Error::new_other)?; + } + + std::fs::write(&path, content).map_err(starlark::Error::new_other)?; + + // On Windows, executability is determined by file extension, + // so no permission bits need to be modified. + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + // On unix, std::fs::write preserves existing permissions. + // So in case the user changed the is_executable flag, we need to set the + // permissions explicitly every single time. + std::fs::set_permissions( + &path, + std::fs::Permissions::from_mode(if is_executable { 0o755 } else { 0o644 }), + ) + .map_err(starlark::Error::new_other)?; + } + + Ok(starlark::values::none::NoneType) + } + } + }; +}
diff --git a/src/gn/starlark/crates/rule/src/ctx.rs b/src/gn/starlark/crates/rule/src/ctx.rs index d0d0ec5..35ca4f6 100644 --- a/src/gn/starlark/crates/rule/src/ctx.rs +++ b/src/gn/starlark/crates/rule/src/ctx.rs
@@ -20,6 +20,10 @@ pub struct Ctx<'v, C: EvalContextAttrExt> { /// Contains ctx.attr/files/file attrs: CtxAttr<'v>, + /// Target label. + label: Value<'v>, + /// The actions object. + actions: Value<'v>, /// The rule currently being evaluated. /// If you have a parent and child rule, this will start as [child], then /// when you call ctx.super() it will be [child, parent]. @@ -28,9 +32,16 @@ } impl<'v, C: EvalContextAttrExt> Ctx<'v, C> { - pub fn new(attrs: CtxAttr<'v>, rule: &'v Rule<'v, C>) -> Self { + pub fn new( + attrs: CtxAttr<'v>, + rule: &'v Rule<'v, C>, + label: Value<'v>, + actions: Value<'v>, + ) -> Self { Self { attrs, + label, + actions, rule_stack: RefCell::new(vec![rule]), } } @@ -73,6 +84,8 @@ self.attrs.attr.trace(tracer); self.attrs.files.trace(tracer); self.attrs.file.trace(tracer); + self.label.trace(tracer); + self.actions.trace(tracer); } } @@ -93,15 +106,23 @@ fn get_attr(&self, attribute: &str, _heap: Heap<'v>) -> Option<Value<'v>> { match attribute { + "actions" => Some(self.actions), "attr" => Some(self.attrs.attr), "files" => Some(self.attrs.files), "file" => Some(self.attrs.file), + "label" => Some(self.label), _ => None, } } fn dir_attr(&self) -> Vec<String> { - vec!["attr".to_owned(), "files".to_owned(), "file".to_owned()] + vec![ + "actions".to_owned(), + "attr".to_owned(), + "files".to_owned(), + "file".to_owned(), + "label".to_owned(), + ] } } @@ -141,11 +162,18 @@ } } + $crate::impl_actions_methods!($ctx_type); + impl $crate::CtxMethods for $ctx_type { fn methods() -> &'static starlark::environment::Methods { starlark::methods_static!(RES = ctx_methods); RES.methods() } + + fn actions_methods() -> &'static starlark::environment::Methods { + starlark::methods_static!(RES = actions_methods); + RES.methods() + } } }; }
diff --git a/src/gn/starlark/crates/rule/src/implementation.rs b/src/gn/starlark/crates/rule/src/implementation.rs index 0cb03c9..e42d959 100644 --- a/src/gn/starlark/crates/rule/src/implementation.rs +++ b/src/gn/starlark/crates/rule/src/implementation.rs
@@ -45,9 +45,16 @@ &eval.heap(), )?, rule, + eval.heap().alloc(target.label().to_owned()), + eval.heap().alloc(crate::actions::Actions::<C>::default()), )); - eval.eval_function(rule.implementation, &[ctx], &[])? + let res = eval.eval_function(rule.implementation, &[ctx], &[])?; + rule_context + .require_rule_impl()? + .borrow() + .rule_impl_complete()?; + res }); let frozen = module.freeze()?;
diff --git a/src/gn/starlark/crates/rule/src/lib.rs b/src/gn/starlark/crates/rule/src/lib.rs index cab2bb1..fe77928 100644 --- a/src/gn/starlark/crates/rule/src/lib.rs +++ b/src/gn/starlark/crates/rule/src/lib.rs
@@ -1,9 +1,12 @@ +pub mod actions; pub mod ctx; pub mod errors; pub mod globals; pub mod implementation; pub mod rule; +pub use actions::Actions; +pub use args; pub use attr::AttrSchema; pub use ctx::Ctx; pub use errors::Error;
diff --git a/src/gn/starlark/crates/rule/tests/actions.rs b/src/gn/starlark/crates/rule/tests/actions.rs new file mode 100644 index 0000000..45d9654 --- /dev/null +++ b/src/gn/starlark/crates/rule/tests/actions.rs
@@ -0,0 +1,157 @@ +// 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::path::Path; + +use anyhow::Context as _; +use rule::run; +use tempfile::tempdir; +use testutils::Assert; +use types::{Label, PackageRef, Session}; + +#[test] +fn test_actions_suite() { + let _temp_dir = tempdir().unwrap(); + let mut assert = Assert::new_rule_assert(); + assert.modify_globals(|builder| { + depset::depset_globals!(builder, testutils::FakeEvalContext); + }); + assert.load_module("//rules:actions.scl"); + assert.pass( + r#" +load( + "//rules:actions.scl", + "already_generated", + "duplicate_declare_file", + "generated_file", + "label", + "undeclared_output", + "write", +) + +write(name = "write") + +label(name = "label") + +generated_file( + name = "empty_filename", + files = {"": ""} +) + +generated_file( + name = "simple_filename", + files = {"simple/filename": "content"} +) + +generated_file( + name = "relative_file", + files = {"../dummy": ""}, +) + +generated_file( + name = "backslashes", + files = {"with\\backslashes": ""}, +) + +generated_file( + name = "never_generated", + files = {"dummy": ""}, +) + +duplicate_declare_file(name = "duplicate_declare_file") + +already_generated(name = "already_generated") + +undeclared_output(name = "undeclared_output", src = "foo/file.cc") +"#, + ); + + type OutputFile<'a> = (&'a Path, &'a str); + type TestCase<'a> = (&'a str, Result<(&'a str, &'a [OutputFile<'a>]), &'a str>); + + let test_cases: &[TestCase<'_>] = &[ + ( + "write", + Ok(( + "obj/write/declared.txt", + &[ + (Path::new("obj/write/declared.txt"), "//:write"), + (Path::new("obj/write/undeclared.txt"), "undeclared"), + ], + )), + ), + ( + "label", + Ok(("", &[(Path::new("obj/label/label.txt"), "label")])), + ), + ( + "simple_filename", + Ok(( + "obj/simple_filename/simple/filename", + &[(Path::new("obj/simple_filename/simple/filename"), "content")], + )), + ), + ( + "empty_filename", + Err("Invalid ctx.actions.declare_file filename: \"\""), + ), + ( + "relative_file", + Err("Invalid ctx.actions.declare_file filename: \"../dummy\""), + ), + ( + "backslashes", + Err("Invalid ctx.actions.declare_file filename: \"with\\\\backslashes\""), + ), + ("duplicate_declare_file", Err("was already declared")), + ("already_generated", Err("has already been generated")), + ("undeclared_output", Err("was not declared by this target")), + ("never_generated", Err("was never generated")), + ]; + + let context = assert.context(); + for &(target_name, expected) in test_cases { + let label = Label::new(PackageRef::root().to_owned(), target_name.to_owned()); + let target = context + .session + .get_target(label.as_ref(), context.session.default_toolchain.as_ref()); + let session = assert.session(); + let res = run(&target, move |t: &testutils::FakeTargetRef| { + testutils::FakeEvalContext::rule_impl(session.clone(), t.clone()) + }); + + match expected { + Ok((defaultinfo_phony, files)) => { + let rooted = res.expect(target_name); + let providers = providers::Providers::try_from(rooted).expect(target_name); + + if defaultinfo_phony.is_empty() { + assert_eq!(providers.outputs_phony, None); + } else { + assert_eq!( + providers.outputs_phony.unwrap().as_path(), + Path::new(defaultinfo_phony) + ); + } + + for &(file_path, expected_content) in files { + let resolved = context + .path_resolver + .resolve(&types::File::intern(&file_path.to_string_lossy())); + let content = std::fs::read_to_string(&resolved) + .context(resolved.display().to_string()) + .unwrap(); + assert_eq!(content, expected_content); + } + }, + Err(expected_error) => { + let err = res.expect_err(target_name); + assert!( + err.to_string().contains(expected_error), + "Target '{target_name}' error '{err}' did not contain expected substring '{expected_error}'" + ); + }, + } + } +}
diff --git a/src/gn/starlark/crates/types/Cargo.toml b/src/gn/starlark/crates/types/Cargo.toml index 120aacb..e1280a3 100644 --- a/src/gn/starlark/crates/types/Cargo.toml +++ b/src/gn/starlark/crates/types/Cargo.toml
@@ -12,6 +12,7 @@ [dependencies] allocative = { workspace = true } cxx = { workspace = true } +normalize-path = { workspace = true } starlark = { workspace = true } starlark_derive = { workspace = true } strum = { workspace = true }
diff --git a/src/gn/starlark/crates/types/src/ctx_state.rs b/src/gn/starlark/crates/types/src/ctx_state.rs index 2cb10f5..9004a30 100644 --- a/src/gn/starlark/crates/types/src/ctx_state.rs +++ b/src/gn/starlark/crates/types/src/ctx_state.rs
@@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +use std::path::{Component, Path}; + use starlark::collections::SmallSet; use crate::{File, TargetRef}; @@ -14,7 +16,9 @@ pub target: T, /// All files declared by ctx.actions.declare_file that were never /// generated. - pub unused_declared_outputs: SmallSet<File>, + unused_declared_outputs: SmallSet<File>, + /// All files declared by ctx.actions.declare_file. + declared_outputs: SmallSet<File>, /// A list of phonies declared during this execution step. pub phonies: Vec<(File, Vec<File>)>, } @@ -25,6 +29,7 @@ Self { target, unused_declared_outputs: SmallSet::new(), + declared_outputs: SmallSet::new(), phonies: Vec::new(), } } @@ -49,7 +54,15 @@ } /// Declares a new output file relative to the target's output directory. - pub fn declare_file(&mut self, name: &str) -> File { + pub fn declare_file(&mut self, name: &str) -> starlark::Result<File> { + if name.is_empty() + // Even on windows, for declare_file, we require using / for a path separator. + || name.contains('\\') + // Validate that the path is both relative and normalized. + || !Path::new(name).components().all(|c| matches!(c, Component::Normal(_))) + { + return Err(crate::errors::Error::UnsupportedFilename(name.to_owned()).into()); + } let mut path = String::new(); if !self.target.is_default_toolchain() { path.push_str(self.target.toolchain().name()); @@ -65,7 +78,31 @@ path.push('/'); path.push_str(name); let file = File::intern(&path); + if !self.declared_outputs.insert(file.clone()) { + return Err(crate::errors::Error::DuplicateDeclaredOutput(file).into()); + } self.unused_declared_outputs.insert(file.clone()); - file + Ok(file) + } + + /// Declares that we are generating a given file. + pub fn generates_file(&mut self, f: &File) -> starlark::Result<()> { + if !self.declared_outputs.contains(f) { + return Err(crate::errors::Error::OutputNotDeclaredByTarget(f.clone()).into()); + } + if !self.unused_declared_outputs.shift_remove(f) { + return Err(crate::errors::Error::OutputAlreadyGenerated(f.clone()).into()); + } + Ok(()) + } + + /// Verifies that all declared outputs were generated. + pub fn rule_impl_complete(&self) -> starlark::Result<()> { + match self.unused_declared_outputs.first() { + Some(unused) => { + Err(crate::errors::Error::DeclaredOutputNeverGenerated(unused.clone()).into()) + }, + None => Ok(()), + } } }
diff --git a/src/gn/starlark/crates/types/src/errors.rs b/src/gn/starlark/crates/types/src/errors.rs index c88aa1d..646d13c 100644 --- a/src/gn/starlark/crates/types/src/errors.rs +++ b/src/gn/starlark/crates/types/src/errors.rs
@@ -24,6 +24,21 @@ FileNotFound(Package, String), #[error("Invalid package, must start with \"//\": \"{0}\"")] NotAPackage(String), + /// The output file was not declared by this target. + #[error("Output file '{0}' was not declared by this target")] + OutputNotDeclaredByTarget(crate::File), + /// The output file has already been generated. + #[error("Output file '{0}' has already been generated")] + OutputAlreadyGenerated(crate::File), + /// Declared file name is unsupported. + #[error("Invalid ctx.actions.declare_file filename: {0:?}")] + UnsupportedFilename(String), + /// The output file was already declared. + #[error("Output file '{0}' was already declared")] + DuplicateDeclaredOutput(crate::File), + /// Declared output file was never generated. + #[error("Declared output file '{0}' was never generated")] + DeclaredOutputNeverGenerated(crate::File), } impl From<Error> for starlark::Error {
diff --git a/src/gn/starlark/crates/types/src/eval_context.rs b/src/gn/starlark/crates/types/src/eval_context.rs index bbe0254..d89ba2e 100644 --- a/src/gn/starlark/crates/types/src/eval_context.rs +++ b/src/gn/starlark/crates/types/src/eval_context.rs
@@ -80,4 +80,5 @@ pub trait CtxMethods { fn methods() -> &'static starlark::environment::Methods; + fn actions_methods() -> &'static starlark::environment::Methods; }
diff --git a/src/gn/starlark/crates/types/src/path_resolver.rs b/src/gn/starlark/crates/types/src/path_resolver.rs index 9cdf05d..66d274a 100644 --- a/src/gn/starlark/crates/types/src/path_resolver.rs +++ b/src/gn/starlark/crates/types/src/path_resolver.rs
@@ -4,6 +4,8 @@ use std::path::PathBuf; +use normalize_path::NormalizePath as _; + use crate::{File, PackageRef}; /// Resolves paths to Files relative to the root_build_dir. @@ -24,8 +26,8 @@ pub fn new(source_root: PathBuf, build_root: PathBuf, source_root_rel: String) -> Self { assert!(source_root_rel.ends_with('/')); Self { - source_root, - build_root, + source_root: source_root.normalize(), + build_root: build_root.normalize(), source_root_rel, } } @@ -42,10 +44,7 @@ /// directory. pub fn resolve(&self, file: &File) -> PathBuf { let joined = self.build_root.join(file.as_path()); - debug_assert!(joined - .canonicalize() - .unwrap() - .starts_with(self.build_root.clone())); + debug_assert!(joined.normalize().starts_with(&self.build_root)); joined }
diff --git a/src/gn/starlark/src/testdata/rules/actions.scl b/src/gn/starlark/src/testdata/rules/actions.scl new file mode 100644 index 0000000..162fc5b --- /dev/null +++ b/src/gn/starlark/src/testdata/rules/actions.scl
@@ -0,0 +1,84 @@ +# 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. + +ActionInfo = provider(fields = ["out_file"]) + +def _write_impl(ctx): + declared = ctx.actions.declare_file("declared.txt") + ctx.actions.write(output = declared, content = str(ctx.label)) + undeclared = ctx.actions.declare_file("undeclared.txt") + ctx.actions.write(output = undeclared, content = "undeclared") + return [ + DefaultInfo(files = depset([declared])), + ActionInfo(out_file = undeclared), + ] + +write = rule( + implementation = _write_impl, +) + +def _label_impl(ctx): + out = ctx.actions.declare_file(ctx.label.name + ".txt") + ctx.actions.write( + output = out, + content = "label", + ) + return [ActionInfo(out_file = out)] + +label = rule( + implementation = _label_impl, +) + +def _generated_file_impl(ctx): + files = [] + for k, v in ctx.attr.files.items(): + f = ctx.actions.declare_file(k) + files.append(f) + if v: + ctx.actions.write(output = f, content = v) + return [DefaultInfo(files = depset(files))] + +generated_file = rule( + implementation = _generated_file_impl, + attrs = {"files": attr.string_dict()}, +) + +def _duplicate_declare_file_impl(ctx): + ctx.actions.declare_file("dup.txt") + ctx.actions.declare_file("dup.txt") + return [] + +duplicate_declare_file = rule( + implementation = _duplicate_declare_file_impl, +) + +def _already_generated_impl(ctx): + out = ctx.actions.declare_file("gen.txt") + ctx.actions.write( + output = out, + content = "first", + ) + ctx.actions.write( + output = out, + content = "second", + ) + return [] + +already_generated = rule( + implementation = _already_generated_impl, +) + +def _undeclared_output_impl(ctx): + ctx.actions.write( + output = ctx.files.src[0], + content = "fail", + ) + return [] + +undeclared_output = rule( + implementation = _undeclared_output_impl, + attrs = { + "src": attr.label(allow_files = True), + }, +)