From 6808359057dfc9504f2ed8459dfd8fa34473c716 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 16:57:27 +0200 Subject: [PATCH 01/14] test cases: add test for rust.test with structured_sources Signed-off-by: Paolo Bonzini --- test cases/rust/9 unit tests/meson.build | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test cases/rust/9 unit tests/meson.build b/test cases/rust/9 unit tests/meson.build index 7a2dba6e148c..57272e53a1ea 100644 --- a/test cases/rust/9 unit tests/meson.build +++ b/test cases/rust/9 unit tests/meson.build @@ -49,8 +49,12 @@ if rustdoc.found() endif exe = executable('rust_exe', ['test2.rs', 'test.rs'], build_by_default : false) +rust.test('rust_test_from_exe_xfail', exe, expected_fail : true) +rust.test('rust_test_from_exe', exe, args: ['--skip', 'test_add_intentional_fail']) -rust.test('rust_test_from_exe', exe, expected_fail : true) +test2_gen = configure_file(input: 'test2.rs', output: 'main.rs', copy: true) +exe_ss = executable('rust_exe_ss', structured_sources([test2_gen, 'test.rs']), build_by_default : false) +rust.test('rust_test_from_exe_ss', exe_ss, args: ['--skip', 'test_add_intentional_fail']) lib = static_library('rust_static', ['test.rs'], build_by_default : false, rust_abi: 'c') rust.test('rust_test_from_static', lib, args: ['--skip', 'test_add_intentional_fail']) From d6ba9b73e435279739b05ef00eaac109271da7e2 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 15:40:24 +0200 Subject: [PATCH 02/14] backends, build: add a single function to determine what is a header_dep Information on which generated files are handled as mere orderdeps is split between ninjabackend and vs2010backend. Create a single function for that. Signed-off-by: Paolo Bonzini --- mesonbuild/backend/ninjabackend.py | 24 +++++++++--------------- mesonbuild/backend/vs2010backend.py | 4 +--- mesonbuild/compilers/__init__.py | 2 ++ mesonbuild/compilers/compilers.py | 4 ++++ 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 8ab7f324399b..8c83221061b5 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -1015,15 +1015,14 @@ def generate_target(self, target: T.Union[build.Target]) -> None: generated_source_files.append(raw_src) elif compilers.is_object(rel_src): obj_list.append(rel_src) - elif compilers.is_library(rel_src) or modules.is_module_library(rel_src): - pass - elif is_compile_target: - generated_source_files.append(raw_src) - else: - # Assume anything not specifically a source file is a header. This is because - # people generate files with weird suffixes (.inc, .fh) that they then include - # in their source files. - header_deps.append(raw_src) + elif compilers.is_unknown(rel_src): + if is_compile_target: + generated_source_files.append(raw_src) + else: + # Assume anything not specifically a source file is a header. This is because + # people generate files with weird suffixes (.inc, .fh) that they then include + # in their source files. + header_deps.append(raw_src) # For D language, the object of generated source files are added # as order only deps because other files may depend on them @@ -1911,12 +1910,7 @@ def generate_cython_transpile(self, target: build.BuildTarget) -> \ cython_sources.append(output) else: generated_sources[ssrc] = mesonlib.File.from_built_file(builddir, ssrc) - # Following logic in L883-900 where we determine whether to add generated source - # as a header(order-only) dep to the .so compilation rule - if not compilers.is_source(ssrc) and \ - not compilers.is_object(ssrc) and \ - not compilers.is_library(ssrc) and \ - not modules.is_module_library(ssrc): + if compilers.is_unknown(ssrc): header_deps.append(ssrc) for source in pyx_sources: source.add_orderdep(header_deps) diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py index ceedf6d7a9c9..b2558eaa9066 100644 --- a/mesonbuild/backend/vs2010backend.py +++ b/mesonbuild/backend/vs2010backend.py @@ -614,9 +614,7 @@ def split_sources(self, srclist: list[FileLike]) -> tuple[list[FileLike], list[F lang = self.lang_from_source_file(i) if lang not in languages: languages.append(lang) - elif compilers.is_library(i): - pass - else: + elif compilers.is_unknown(i): # Everything that is not an object or source file is considered a header. headers.append(i) return sources, headers, objects, languages diff --git a/mesonbuild/compilers/__init__.py b/mesonbuild/compilers/__init__.py index f645090e195b..8f412ea3b415 100644 --- a/mesonbuild/compilers/__init__.py +++ b/mesonbuild/compilers/__init__.py @@ -22,6 +22,7 @@ 'is_source', 'is_java', 'is_known_suffix', + 'is_unknown', 'lang_suffixes', 'LANGUAGES_USING_LDFLAGS', 'sort_clink', @@ -64,6 +65,7 @@ is_library, is_known_suffix, is_separate_compile, + is_unknown, lang_suffixes, LANGUAGES_USING_LDFLAGS, sort_clink, diff --git a/mesonbuild/compilers/compilers.py b/mesonbuild/compilers/compilers.py index 0ba0adbca4e3..09cb72a07fa8 100644 --- a/mesonbuild/compilers/compilers.py +++ b/mesonbuild/compilers/compilers.py @@ -208,6 +208,10 @@ def is_known_suffix(fname: 'mesonlib.FileOrString') -> bool: return suffix in all_suffixes +def is_unknown(src: mesonlib.FileOrString) -> bool: + from ..modules import is_module_library + return not (is_source(src) or is_object(src) or is_library(src) or is_module_library(src)) + class CompileCheckMode(enum.Enum): From d34b6033e195fddfda5742ae6f077d77d6009f5e Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 13:48:04 +0200 Subject: [PATCH 03/14] ninjabackend: make the description of GeneratedLists more complete List both the output (if only one) and the source. Signed-off-by: Paolo Bonzini --- mesonbuild/backend/ninjabackend.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 8c83221061b5..f156914c2e0e 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -2870,14 +2870,13 @@ def generate_genlist_for_target(self, genlist: build.GeneratedList, target: buil if generator.depfile is not None: elem.add_item('DEPFILE', depfile) + desc = 'Generating ' if len(generator.outputs) == 1: - what = f'{sole_output!r}' - else: - # since there are multiple outputs, we log the source that caused the rebuild - what = f'from {sole_output!r}' + desc += f'{outfilespriv[0]!r} ' + desc += f'from {curfile!r}' if reason: - reason = f' (wrapped by meson {reason})' - elem.add_item('DESC', f'Generating {what}{reason}') + desc += f' (wrapped by meson {reason})' + elem.add_item('DESC', desc) elem.add_item('COMMAND', cmdlist) self.add_build(elem) From 1860e06e78182912029adc9160782196867ca792 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 15:29:53 +0200 Subject: [PATCH 04/14] build: include computed fields of GeneratedList in repr This simplifies debugging, since all the interesting information is added to these fields by Generator.process_files. Signed-off-by: Paolo Bonzini --- mesonbuild/build.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 2f00aa17d441..cfc904615905 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -2132,14 +2132,14 @@ class GeneratedList(HoldableObject): extra_args: T.List[str] env: T.Optional[EnvironmentVariables] extra_depends: T.List[TargetDepends] + depends: set[GeneratedTypes] = field(default_factory=set, init=False) + infilelist: list[FileMaybeInTargetPrivateDir] = field(default_factory=list, init=False) + outfilelist: list[str] = field(default_factory=list, init=False) + outmap: dict[FileMaybeInTargetPrivateDir, list[str]] = field(default_factory=dict, init=False) + depend_files: list[File] = field(default_factory=list, init=False) def __post_init__(self) -> None: self.name = self.generator.exe - self.depends: T.Set[GeneratedTypes] = set() - self.infilelist: T.List[FileMaybeInTargetPrivateDir] = [] - self.outfilelist: T.List[str] = [] - self.outmap: T.Dict[FileMaybeInTargetPrivateDir, T.List[str]] = {} - self.depend_files: T.List[File] = [] if self.extra_args is None: self.extra_args: T.List[str] = [] From b25141322f139681b0b5b667173f346adfcb8ec7 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 15:05:39 +0200 Subject: [PATCH 05/14] scripts: do not assume directory is created before copy Prepare for adding structured_sources support to backends other than ninja. Signed-off-by: Paolo Bonzini --- mesonbuild/scripts/copy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mesonbuild/scripts/copy.py b/mesonbuild/scripts/copy.py index 73908638b852..1e07d4150ea3 100644 --- a/mesonbuild/scripts/copy.py +++ b/mesonbuild/scripts/copy.py @@ -7,12 +7,18 @@ This is easier than trying to detect whether to use copy, cp, or something else. """ +import os import shutil import typing as T def run(args: T.List[str]) -> int: try: + # The destination may be in a subdirectory that does not exist yet (for + # example when copying structured sources into the build tree). Ninja + # creates output directories on its own, but other backends do not, so + # make sure the destination directory exists. + os.makedirs(os.path.dirname(args[1]) or '.', exist_ok=True) shutil.copy2(args[0], args[1]) except Exception: return 1 From 7f25e1f6846b75c3a754fb2254e613991e3ad1c8 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 14:55:37 +0200 Subject: [PATCH 06/14] build: add output_subdir to GeneratedList This is not exposed via generator.process(), but it can be used by the conversion of StructuredSources to GeneratedLists. Signed-off-by: Paolo Bonzini --- mesonbuild/build.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index cfc904615905..b43634d9b4c5 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -2086,14 +2086,16 @@ def process_files(self, files: T.Iterable[T.Union[str, File, GeneratedTypes]], preserve_path_from: T.Optional[str] = None, extra_args: T.Optional[T.List[str]] = None, env: T.Optional[EnvironmentVariables] = None, - extra_depends: T.Optional[T.Sequence[TargetDepends]] = None) -> 'GeneratedList': + extra_depends: T.Optional[T.Sequence[TargetDepends]] = None, + output_subdir: str = '') -> 'GeneratedList': output = GeneratedList( self, subdir, preserve_path_from, extra_args=extra_args if extra_args is not None else [], env=env if env is not None else EnvironmentVariables(), - extra_depends=list(extra_depends) if extra_depends is not None else []) + extra_depends=list(extra_depends) if extra_depends is not None else [], + output_subdir=output_subdir) for e in files: if isinstance(e, (CustomTarget, CustomTargetIndex)): @@ -2132,6 +2134,11 @@ class GeneratedList(HoldableObject): extra_args: T.List[str] env: T.Optional[EnvironmentVariables] extra_depends: T.List[TargetDepends] + # An extra directory, relative to the target private dir, that the outputs + # are placed in. Used to reproduce the directory layout of structured + # sources without baking the path into the (shared) generator's template. + output_subdir: str = '' + depends: set[GeneratedTypes] = field(default_factory=set, init=False) infilelist: list[FileMaybeInTargetPrivateDir] = field(default_factory=list, init=False) outfilelist: list[str] = field(default_factory=list, init=False) @@ -2182,6 +2189,8 @@ def add_file(self, newfile: FileMaybeInTargetPrivateDir) -> None: if self.preserve_path_from: path_segment = self.get_preserved_path_segment(newfile) outfiles = [os.path.join(path_segment, of) for of in outfiles] + if self.output_subdir: + outfiles = [os.path.join(self.output_subdir, of) for of in outfiles] self.outfilelist += outfiles self.outmap[newfile] = outfiles From 9e272369b380c5193d624cb82eba3eb3c1fd7268 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 16:22:11 +0200 Subject: [PATCH 07/14] build: generalize check for incorrect use of structured_sources Apply it to any language that does not support separate compilation. This is still only Rust, but it provides the foundation for transforming structured sources into normal ones. Signed-off-by: Paolo Bonzini --- mesonbuild/build.py | 31 ++++++++++++++----- .../rust/19 structured sources/meson.build | 4 +++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index b43634d9b4c5..92ac5fe57107 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -31,7 +31,7 @@ from .compilers import ( is_header, is_object, is_source, clink_langs, sort_clink, all_languages, - is_known_suffix, detect_static_linker, LANGUAGES_USING_LDFLAGS, + is_known_suffix, is_separate_compile, detect_static_linker, LANGUAGES_USING_LDFLAGS, get_base_compile_args ) from .interpreterbase import FeatureNew, FeatureDeprecated @@ -908,6 +908,8 @@ def __init__( self.swift_interoperability_mode = kwargs.get('swift_interoperability_mode', 'c') self.swift_module_name = kwargs.get('swift_module_name') or self.name + if self.structured_sources: + self.process_structured_sources() self.missing_languages = self.process_compilers() self.single_compile_base_args: T.Dict[Compiler, ImmutableListProtocol[str]] = {} @@ -975,12 +977,6 @@ def post_init(self) -> None: if self.uses_rust(): if self.link_language and self.link_language != 'rust': raise MesonException('cannot build Rust sources with a different link_language') - if self.structured_sources: - # TODO: the interpreter should be able to generate a better error message? - if any((s.endswith('.rs') for s in self.sources)) or \ - any(any((s.endswith('.rs') for s in g.get_outputs())) for g in self.generated): - raise MesonException('cannot mix Rust structured sources and unstructured sources') - # relocation-model=pic is rustc's default and Meson does not # currently have a way to disable PIC. self.pic = True @@ -1003,6 +999,27 @@ def post_init(self) -> None: for compiler in self.compilers.values(): self.single_compile_base_args[compiler] = self._generate_single_compile_base_args(compiler) + def process_structured_sources(self) -> None: + source_suffixes = set() + for s in itertools.chain(self.sources, *(g.get_outputs() for g in self.generated)): + assert isinstance(s, (File, str)), 'for mypy' + if isinstance(s, File): + s = s.fname + if is_separate_compile(s): + continue + suffix = s.split('.')[-1] + source_suffixes.add('.' + suffix) + + for v in self.structured_sources.sources.values(): + for src in v: + if isinstance(src, (str, File)): + items = (src,) + else: + items = src.get_outputs() + for suffix in source_suffixes: + if any(s.endswith(suffix) for s in items): + raise MesonException(f'cannot mix {suffix!r} files in structured and unstructured sources') + def __repr__(self) -> str: repr_str = "<{0} {1}: {2}>" return repr_str.format(self.__class__.__name__, self.get_id(), self.filename) diff --git a/test cases/rust/19 structured sources/meson.build b/test cases/rust/19 structured sources/meson.build index f1925835ac42..3167f6b1c9bb 100644 --- a/test cases/rust/19 structured sources/meson.build +++ b/test cases/rust/19 structured sources/meson.build @@ -58,3 +58,7 @@ m_src2 = configure_file( executable('gen-no-copy', structured_sources([m_src, m_src2])) executable('gen-no-copy-with-non-rust', structured_sources(['empty.file', m_src, m_src2])) + +testcase expect_error('cannot mix \'.rs\' files in structured and unstructured sources') + executable('bad', structured_sources([m_src]), m_src2) +endtestcase From da24a97ef4266f304b0287b09bd039945e30072d Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 14:51:18 +0200 Subject: [PATCH 08/14] build, ninjabackend: add support for Generator description Similar to CustomTargets, and likewise not exposed by the Meson DSL. Signed-off-by: Paolo Bonzini --- mesonbuild/backend/ninjabackend.py | 11 +++++++---- mesonbuild/build.py | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index f156914c2e0e..327b2340ac7c 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -2870,10 +2870,13 @@ def generate_genlist_for_target(self, genlist: build.GeneratedList, target: buil if generator.depfile is not None: elem.add_item('DEPFILE', depfile) - desc = 'Generating ' - if len(generator.outputs) == 1: - desc += f'{outfilespriv[0]!r} ' - desc += f'from {curfile!r}' + if generator.description is not None: + desc = generator.description.format(input=curfile, output=outfilespriv[0]) + else: + desc = 'Generating ' + if len(generator.outputs) == 1: + desc += f'{outfilespriv[0]!r} ' + desc += f'from {curfile!r}' if reason: desc += f' (wrapped by meson {reason})' elem.add_item('DESC', desc) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 92ac5fe57107..22e48075110d 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -2063,7 +2063,8 @@ def __init__(self, env: Environment, depfile: T.Optional[str] = None, capture: bool = False, depends: T.Optional[T.Sequence[TargetDepends]] = None, - name: str = 'Generator'): + name: str = 'Generator', + description: T.Optional[str] = None): self.environment = env self.exe = exe self.depfile = depfile @@ -2072,6 +2073,9 @@ def __init__(self, env: Environment, self.arglist = arguments self.outputs = output self.name = name + # A str.format() template used by the backend for the build progress + # message, with '{input}' and '{output}' fields. + self.description = description def __repr__(self) -> str: repr_str = "<{0}: {1}>" From 82301181de95e6d43a11def12c6996f29beebe27 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 14:43:24 +0200 Subject: [PATCH 09/14] build: add a hardcoded Generator to copy from structured sources Signed-off-by: Paolo Bonzini --- mesonbuild/build.py | 12 ++++++++++++ mesonbuild/environment.py | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 22e48075110d..14bdcf6a231a 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -2237,6 +2237,18 @@ def get_basename(self) -> str: return self.generator.name +def get_copy_generator(environment: Environment) -> Generator: + """Return a shared Generator that copies a file using `meson --internal copy`.""" + if environment.copy_generator is None: + exe = programs.ExternalProgram( + 'meson', command=environment.get_build_command() + ['--internal', 'copy'], + silent=True) + environment.copy_generator = Generator( + environment, exe, ['@INPUT@', '@OUTPUT@'], ['@PLAINNAME@'], + name='copy', description='Copying {output} from {input}') + return environment.copy_generator + + class Executable(BuildTarget, LinkableTarget): typename = 'executable' diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py index de5daa48b4fb..1c86bf6e8602 100644 --- a/mesonbuild/environment.py +++ b/mesonbuild/environment.py @@ -33,6 +33,7 @@ from mesonbuild import envconfig if T.TYPE_CHECKING: + from .build import Generator from .compilers.compilers import Compiler, CompilerDict, Language from .options import OptionDict, ElementaryOptionValues from .wrap.wrap import Resolver @@ -232,6 +233,9 @@ def __init__(self, source_dir: str, build_dir: T.Optional[str], cmd_options: cmd self.default_cmake = ['cmake'] self.default_pkgconfig = ['pkg-config'] self.wrap_resolver: T.Optional['Resolver'] = None + # Lazily created shared Generator used to copy structured sources into + # the build tree (see build.get_copy_generator). + self.copy_generator: T.Optional['Generator'] = None def mfilestr2key(self, machine_file_string: str, section: T.Optional[str], section_subproject: T.Optional[str], machine: MachineChoice) -> OptionKey: key = OptionKey.from_string(machine_file_string) From f35547ae177bbe5c0a3537a963e70b156cd88934 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 15:03:49 +0200 Subject: [PATCH 10/14] build, ninjabackend: use GeneratedList for jar resources Use GeneratedLists to copy all resources into the root of the jar file. No resources argument is treated simply as an empty list. Signed-off-by: Paolo Bonzini --- mesonbuild/backend/ninjabackend.py | 6 ++++-- mesonbuild/build.py | 21 +++++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 327b2340ac7c..52f6d6791720 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -1534,9 +1534,11 @@ def generate_jar_target(self, target: build.Jar) -> None: commands += ['-C', self.get_target_private_dir(target), '.'] elem = NinjaBuildElement(self.all_outputs, outname_rel, jar_rule, []) elem.add_dep(class_dep_list) - if resources: + for gl in resources: # Copy all resources into the root of the jar. - elem.add_orderdep(self.__generate_sources_structure(Path(self.get_target_private_dir(target)), resources)[0]) + self.generate_genlist_for_target(gl, target) + elem.add_orderdep([os.path.join(self.get_target_private_dir(target), o) + for o in gl.get_outputs()]) elem.add_item('ARGS', commands) self.add_build(elem) # Create introspection information diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 14bdcf6a231a..27d7e5ef9fb1 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -999,6 +999,17 @@ def post_init(self) -> None: for compiler in self.compilers.values(): self.single_compile_base_args[compiler] = self._generate_single_compile_base_args(compiler) + def lower_structured_sources(self, struct: StructuredSources, subdir: str) -> T.List[GeneratedList]: + """Turn structured sources into GeneratedLists.""" + generator = get_copy_generator(self.environment) + genlists = [generator.process_files(files, self.subdir, + output_subdir=os.path.join(subdir, path)) + for path, files in struct.sources.items()] + # build the main directory first, so that the first file in the root + # directory is used as the main file for Rust structured_sources. + genlists.sort(key=lambda g: g.output_subdir) + return genlists + def process_structured_sources(self) -> None: source_suffixes = set() for s in itertools.chain(self.sources, *(g.get_outputs() for g in self.generated)): @@ -3420,6 +3431,12 @@ def __init__(self, name: str, subdir: str, subproject: SubProject, for_machine: self.java_args = self.extra_args['java'] self.main_class = kwargs.get('main_class', '') self.java_resources: T.Optional[StructuredSources] = kwargs.get('java_resources', None) + # Resources are always copied into the jar's private directory (so that + # `jar -C .` can pick them up); unlike compiled structured + # sources they are not added to self.generated. + self.java_resource_genlists: T.List[GeneratedList] = [] + if self.java_resources: + self.java_resource_genlists = self.lower_structured_sources(self.java_resources, '') def _extract_link_with(self, kwargs: BuildTargetKeywordArguments) -> list[LinkableTargetTypes]: return kwargs['link_with'] @@ -3433,8 +3450,8 @@ def type_suffix(self) -> str: def get_java_args(self) -> T.List[str]: return self.java_args - def get_java_resources(self) -> T.Optional[StructuredSources]: - return self.java_resources + def get_java_resources(self) -> T.List[GeneratedList]: + return self.java_resource_genlists def validate_install(self) -> None: # All jar targets are installable. From 7e33c9af9460a0436e0b100dae16b9c88cc6cec2 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 17:01:26 +0200 Subject: [PATCH 11/14] build, ninjabackend: use GeneratedList for structured_sources Remove the duplicated code to find the main .rs file across both structured and normal sources, as well as the code to generate copy targets: instead just reuse the GeneratedList mechanism. This provides support across multiple backends, makes Rust less of a special case, and in fact makes structured sources themselves less of a special case. Signed-off-by: Paolo Bonzini --- mesonbuild/backend/ninjabackend.py | 76 +----------------------------- mesonbuild/build.py | 38 ++++++++------- mesonbuild/modules/rust.py | 2 +- 3 files changed, 24 insertions(+), 92 deletions(-) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 52f6d6791720..d41f5f65bcdb 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -1432,8 +1432,6 @@ def generate_rules(self) -> None: self.add_rule(NinjaRule('CUSTOM_COMMAND_MSVC_DEP', ['$COMMAND'], [], '$DESC', deps='msvc', restat=True)) - self.add_rule(NinjaRule('COPY_FILE', self.environment.get_build_command() + ['--internal', 'copy'], - ['$in', '$out'], 'Copying $in to $out')) c = self.environment.get_build_command() + \ ['--internal', @@ -1919,40 +1917,6 @@ def generate_cython_transpile(self, target: build.BuildTarget) -> \ return static_sources, generated_sources, cython_sources - def _generate_copy_target(self, src: FileOrString, output: Path) -> None: - """Create a target to copy a source file from one location to another.""" - if isinstance(src, File): - instr = src.absolute_path(self.environment.source_dir, self.environment.build_dir) - else: - instr = src - elem = NinjaBuildElement(self.all_outputs, [str(output)], 'COPY_FILE', [instr]) - elem.add_orderdep(instr) - self.add_build(elem) - - def __generate_sources_structure(self, root: Path, structured_sources: build.StructuredSources, - main_file_ext: T.Union[str, T.Tuple[str, ...]] = tuple(), - ) -> T.Tuple[T.List[str], T.Optional[str]]: - first_file: T.Optional[str] = None - orderdeps: T.List[str] = [] - for path, files in structured_sources.sources.items(): - for file in files: - if isinstance(file, File): - out = root / path / Path(file.fname).name - self._generate_copy_target(file, out) - out_s = str(out) - orderdeps.append(out_s) - if first_file is None and out_s.endswith(main_file_ext): - first_file = out_s - else: - for f in file.get_outputs(): - out = root / path / f - out_s = str(out) - orderdeps.append(out_s) - self._generate_copy_target(str(Path(file.subdir) / f), out) - if first_file is None and out_s.endswith(main_file_ext): - first_file = out_s - return orderdeps, first_file - def _add_rust_project_entry(self, name: str, main_rust_file: str, args: CompilerArgs, crate_type: str, target_name: str, from_subproject: bool, proc_macro_dylib_path: T.Optional[str], @@ -2006,44 +1970,6 @@ def generate_rust_sources(self, target: build.BuildTarget) -> T.Tuple[T.List[str # figures out what other files are needed via import # statements and magic. main_rust_file: T.Optional[str] = None - if target.structured_sources: - if target.structured_sources.needs_copy(): - _ods, main_rust_file = self.__generate_sources_structure(Path( - self.get_target_private_dir(target)) / 'structured', target.structured_sources, '.rs') - if main_rust_file is None: - raise MesonException('Could not find a rust file to treat as the main file for ', target.name) - else: - # The only way to get here is to have only files in the "root" - # positional argument, which are all generated into the same - # directory - for g in target.structured_sources.sources['']: - if isinstance(g, File): - if g.endswith('.rs'): - main_rust_file = g.rel_to_builddir(self.build_to_src) - elif isinstance(g, GeneratedList): - for h in g.get_outputs(): - if h.endswith('.rs'): - main_rust_file = os.path.join(self.get_target_private_dir(target), h) - break - else: - for h in g.get_outputs(): - if h.endswith('.rs'): - main_rust_file = os.path.join(g.get_builddir(), h) - break - if main_rust_file is not None: - break - - _ods = [] - for f in target.structured_sources.as_list(): - if isinstance(f, File): - _ods.append(f.rel_to_builddir(self.build_to_src)) - else: - _ods.extend([os.path.join(self.build_to_src, f.subdir, s) - for s in f.get_outputs()]) - self.all_structured_sources.update(_ods) - orderdeps.extend(_ods) - return orderdeps, main_rust_file - for i in target.get_sources(): if main_rust_file is None and i.endswith('.rs'): main_rust_file = i.rel_to_builddir(self.build_to_src) @@ -2872,6 +2798,8 @@ def generate_genlist_for_target(self, genlist: build.GeneratedList, target: buil if generator.depfile is not None: elem.add_item('DEPFILE', depfile) + if generator is self.environment.copy_generator: + self.all_structured_sources.update(outfilespriv) if generator.description is not None: desc = generator.description.format(input=curfile, output=outfilespriv[0]) else: diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 27d7e5ef9fb1..2e813181afc4 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -30,7 +30,7 @@ from .options import OptionKey from .compilers import ( - is_header, is_object, is_source, clink_langs, sort_clink, all_languages, + is_header, is_object, is_source, is_unknown, clink_langs, sort_clink, all_languages, is_known_suffix, is_separate_compile, detect_static_linker, LANGUAGES_USING_LDFLAGS, get_base_compile_args ) @@ -927,7 +927,7 @@ def __init__( self._set_vala_args(kwargs) if not any([[src for src in self.sources if not is_header(src)], self.generated, self.objects, - self.link_whole_targets, self.structured_sources, kwargs.pop('_allow_no_sources', False)]): + self.link_whole_targets, kwargs.pop('_allow_no_sources', False)]): mlog.warning(f'Build target {name} has no sources. ' 'This was never supposed to be allowed but did because of a bug, ' 'support will be removed in a future release of Meson') @@ -981,9 +981,6 @@ def post_init(self) -> None: # currently have a way to disable PIC. self.pic = True self.pie = True - else: - if self.structured_sources: - raise MesonException('structured sources are only supported in Rust targets') if self.is_linkable_target(): if self.vala_header is not None: @@ -1011,6 +1008,8 @@ def lower_structured_sources(self, struct: StructuredSources, subdir: str) -> T. return genlists def process_structured_sources(self) -> None: + """Turn structured sources into regular sources or GeneratedLists, + depending on whether a copy into the build tree is needed.""" source_suffixes = set() for s in itertools.chain(self.sources, *(g.get_outputs() for g in self.generated)): assert isinstance(s, (File, str)), 'for mypy' @@ -1031,6 +1030,20 @@ def process_structured_sources(self) -> None: if any(s.endswith(suffix) for s in items): raise MesonException(f'cannot mix {suffix!r} files in structured and unstructured sources') + if self.structured_sources.needs_copy(): + self.generated += self.lower_structured_sources(self.structured_sources, 'structured') + return + + # Every entry is a plain source file that is already laid out correctly + # in the source tree, so it can be used in place. Note that backends + # drop unknown files when generated but not when they are from the source + # tree; since StructuredSources effectively always count as generated, + # drop them here. + for f in self.structured_sources.as_list(): + assert isinstance(f, File) and not f.is_built + if not is_unknown(f.fname): + self.sources.append(f) + def __repr__(self) -> str: repr_str = "<{0} {1}: {2}>" return repr_str.format(self.__class__.__name__, self.get_id(), self.filename) @@ -1179,22 +1192,13 @@ def process_compilers(self) -> T.List[Language]: C/C++ compiler for cython. ''' missing_languages: T.List[Language] = [] - if not any([self.sources, self.generated, self.objects, self.structured_sources]): + if not any([self.sources, self.generated, self.objects]): return missing_languages + # Preexisting sources sources: T.List['FileOrString'] = list(self.sources) - generated = self.generated.copy() - - if self.structured_sources: - for v in self.structured_sources.sources.values(): - for src in v: - if isinstance(src, (str, File)): - sources.append(src) - else: - generated.append(src) - # All generated sources - for gensrc in generated: + for gensrc in self.generated: for s in gensrc.get_outputs(): # Generated objects can't be compiled, so don't use them for # compiler detection. If our target only has generated objects, diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 5e5e8575c0b6..dd3b5ee8b166 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -666,7 +666,7 @@ def test_common(self, funcname: str, state: ModuleState, args: T.Tuple[str, Buil new_target = Executable( name, base_target.subdir, state.subproject, base_target.for_machine, - sources, base_target.structured_sources, + sources, None, base_target.objects, base_target.environment, base_target.compilers, new_target_kwargs) return new_target, tkwargs From 00880d8fc510097c3a1cdd699f1c36efa3ada8eb Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 15:07:09 +0200 Subject: [PATCH 12/14] tests: add testcase for non-Rust structured_sources Signed-off-by: Paolo Bonzini --- .../298 structured sources/generated.c.in | 3 ++ .../298 structured sources/include/inc.h | 4 +++ .../common/298 structured sources/main.c | 5 +++ .../298 structured sources/main_generated.c | 5 +++ .../common/298 structured sources/meson.build | 35 +++++++++++++++++++ .../298 structured sources/sub/helper.c | 3 ++ 6 files changed, 55 insertions(+) create mode 100644 test cases/common/298 structured sources/generated.c.in create mode 100644 test cases/common/298 structured sources/include/inc.h create mode 100644 test cases/common/298 structured sources/main.c create mode 100644 test cases/common/298 structured sources/main_generated.c create mode 100644 test cases/common/298 structured sources/meson.build create mode 100644 test cases/common/298 structured sources/sub/helper.c diff --git a/test cases/common/298 structured sources/generated.c.in b/test cases/common/298 structured sources/generated.c.in new file mode 100644 index 000000000000..e8907745f27b --- /dev/null +++ b/test cases/common/298 structured sources/generated.c.in @@ -0,0 +1,3 @@ +int generated(void) { + return @RETVAL@; +} diff --git a/test cases/common/298 structured sources/include/inc.h b/test cases/common/298 structured sources/include/inc.h new file mode 100644 index 000000000000..68bfea3a3123 --- /dev/null +++ b/test cases/common/298 structured sources/include/inc.h @@ -0,0 +1,4 @@ +#pragma once + +int helper(void); +int generated(void); diff --git a/test cases/common/298 structured sources/main.c b/test cases/common/298 structured sources/main.c new file mode 100644 index 000000000000..c51481e75843 --- /dev/null +++ b/test cases/common/298 structured sources/main.c @@ -0,0 +1,5 @@ +#include "inc.h" + +int main(void) { + return helper(); +} diff --git a/test cases/common/298 structured sources/main_generated.c b/test cases/common/298 structured sources/main_generated.c new file mode 100644 index 000000000000..2b0e59deebfa --- /dev/null +++ b/test cases/common/298 structured sources/main_generated.c @@ -0,0 +1,5 @@ +#include "inc.h" + +int main(void) { + return generated(); +} diff --git a/test cases/common/298 structured sources/meson.build b/test cases/common/298 structured sources/meson.build new file mode 100644 index 000000000000..7d1fb7b16eab --- /dev/null +++ b/test cases/common/298 structured sources/meson.build @@ -0,0 +1,35 @@ +project('structured sources', 'c') + +# Every input already exists in the right place +in_place = executable( + 'in_place', + structured_sources( + ['main.c'], + {'sub' : 'sub/helper.c'}, + ), + include_directories: include_directories('include'), +) +test('in place', in_place, expected_exitcode: 42) + +gen = configure_file( + input : 'generated.c.in', + output : 'generated.c', + configuration : {'RETVAL' : '58'}, +) +copied = executable( + 'copied', + structured_sources( + ['main_generated.c', gen, 'include/inc.h'], + ), +) +test('copied', copied, expected_exitcode: 58) + +# mixing structured and unstructured sources is allowed if compilations +# are issued separately. +mixed = executable( + 'mixed', + 'main_generated.c', + structured_sources([gen]), + include_directories: include_directories('include'), +) +test('mixed', mixed, expected_exitcode: 58) diff --git a/test cases/common/298 structured sources/sub/helper.c b/test cases/common/298 structured sources/sub/helper.c new file mode 100644 index 000000000000..84c49f2d68a2 --- /dev/null +++ b/test cases/common/298 structured sources/sub/helper.c @@ -0,0 +1,3 @@ +int helper(void) { + return 42; +} From b4334daae16caaadfb86b6f0314885037af95955 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 15:07:38 +0200 Subject: [PATCH 13/14] docs: add release notes for non-Rust structured_sources Signed-off-by: Paolo Bonzini --- .../snippets/structured-sources-any-language.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/markdown/snippets/structured-sources-any-language.md diff --git a/docs/markdown/snippets/structured-sources-any-language.md b/docs/markdown/snippets/structured-sources-any-language.md new file mode 100644 index 000000000000..f486174c52ca --- /dev/null +++ b/docs/markdown/snippets/structured-sources-any-language.md @@ -0,0 +1,11 @@ +## `structured_sources()` can be used with any language + +[[structured_sources]] previously only worked with Rust build targets +and Java resources, and raised an error otherwise. It can now be used +with a target of any language (other than Java, where it can only be +used for resources like before). + +Files will be laid out in the build directory according to the structure +described by the dictionary keys; for example, in the case of C +it will be possible to include a file from the `structured_sources()` +from another according to their provided path. From 90925a900f539db718552806c7ddf7417ef5fb6a Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Fri, 5 Jun 2026 19:18:06 +0200 Subject: [PATCH 14/14] dependencies: remove bogus StructuredSources annotation structured_sources is not allowed in declare_dependency()'s sources argument. Remove it, together with all of its ramifications. Signed-off-by: Paolo Bonzini --- mesonbuild/dependencies/base.py | 8 ++++---- mesonbuild/interpreter/interpreter.py | 2 +- mesonbuild/modules/hotdoc.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mesonbuild/dependencies/base.py b/mesonbuild/dependencies/base.py index 51f5822233ca..8d276268ace8 100644 --- a/mesonbuild/dependencies/base.py +++ b/mesonbuild/dependencies/base.py @@ -29,7 +29,7 @@ from ..interpreterbase import FeatureCheckBase from ..build import ( CustomTarget, IncludeDirs, CustomTargetIndex, LinkableTargetTypes, - StaticLibrary, StructuredSources, ExtractedObjects, GeneratedTypes + StaticLibrary, ExtractedObjects, GeneratedTypes ) from ..interpreter.type_checking import PkgConfigDefineType @@ -148,7 +148,7 @@ def __init__(self, kwargs: DependencyObjectKWs) -> None: # Raw -L and -l arguments without manual library searching # If None, self.link_args will be used self.raw_link_args: T.Optional[T.List[str]] = None - self.sources: T.List[T.Union[mesonlib.File, GeneratedTypes, 'StructuredSources']] = [] + self.sources: T.List[T.Union[mesonlib.File, GeneratedTypes]] = [] self.extra_files: T.List[mesonlib.File] = [] self.include_type = kwargs.get('include_type', 'preserve') self.ext_deps: T.List[Dependency] = [] @@ -220,7 +220,7 @@ def get_all_link_args(self) -> T.List[str]: def found(self) -> bool: return self.is_found - def get_sources(self) -> T.List[T.Union[mesonlib.File, GeneratedTypes, 'StructuredSources']]: + def get_sources(self) -> T.List[T.Union[mesonlib.File, GeneratedTypes]]: """Source files that need to be added to the target. As an example, gtest-all.cc when using GTest.""" return self.sources @@ -311,7 +311,7 @@ def __init__(self, version: str, incdirs: T.Optional[T.List['IncludeDirs']] = No link_args: T.Optional[T.List[str]] = None, libraries: T.Optional[T.List[LinkableTargetTypes]] = None, whole_libraries: T.Optional[T.List[T.Union[StaticLibrary, CustomTarget, CustomTargetIndex]]] = None, - sources: T.Optional[T.Sequence[T.Union[mesonlib.File, GeneratedTypes, StructuredSources]]] = None, + sources: T.Optional[T.Sequence[T.Union[mesonlib.File, GeneratedTypes]]] = None, extra_files: T.Optional[T.Sequence[mesonlib.File]] = None, ext_deps: T.Optional[T.List[Dependency]] = None, variables: T.Optional[T.Dict[str, str]] = None, d_module_versions: T.Optional[T.List[T.Union[str, int]]] = None, diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 88c93ac0f9c2..8e26505d5fe0 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -489,7 +489,7 @@ def append_holder_map(self, held_type: T.Type[mesonlib.HoldableObject], holder_t held_type: holder_type }) - def process_new_values(self, invalues: list[TYPE_var | ExecutableSerialisation] | list[build.GeneratedTypes | mesonlib.File | build.StructuredSources]) -> None: + def process_new_values(self, invalues: list[TYPE_var | ExecutableSerialisation] | list[build.GeneratedTypes | mesonlib.File]) -> None: for v in invalues: if isinstance(v, ObjectHolder): raise InterpreterException('Modules must not return ObjectHolders') diff --git a/mesonbuild/modules/hotdoc.py b/mesonbuild/modules/hotdoc.py index 4a3ebdd34c13..08b033753fc9 100644 --- a/mesonbuild/modules/hotdoc.py +++ b/mesonbuild/modules/hotdoc.py @@ -176,8 +176,8 @@ def process_gi_c_source_roots(self) -> None: self.cmd += ['--gi-c-source-roots'] + value - def process_dependencies(self, deps: T.Sequence[TargetDepends | Dependency | File | build.ExtractedObjects | build.StructuredSources]) -> T.List[str]: - # build.StructuredSources and build.ExtractedObjects shouldn't actually + def process_dependencies(self, deps: T.Sequence[TargetDepends | Dependency | File | build.ExtractedObjects]) -> T.List[str]: + # build.ExtractedObjects shouldn't actually # happen here, but we get them from Dependency. cflags = set() for dep in mesonlib.listify(ensure_list(deps)):