From 3a99a86f46dce4c0357e1a5e1e348aed5b4b8d31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Aug 2026 05:01:21 +0000 Subject: [PATCH 1/3] Initial plan From b31454e1ebf888744fa657a1ca4dde521dd5541f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Aug 2026 05:05:07 +0000 Subject: [PATCH 2/3] Warn on duplicate LLSD package definitions --- autobuild/configfile.py | 41 ++++++++++++++++++++++++++++++ tests/test_configfile.py | 55 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/autobuild/configfile.py b/autobuild/configfile.py index 9d03f6b..3032da6 100644 --- a/autobuild/configfile.py +++ b/autobuild/configfile.py @@ -6,6 +6,7 @@ import re import string import sys +import xml.etree.ElementTree as ET from io import StringIO import llsd @@ -37,6 +38,44 @@ class NoVersionFileKeyError(common.AutobuildError): pass +def _iter_duplicate_llsd_keys(element, path=()): + if element.tag == 'map': + seen = set() + children = list(element) + index = 0 + while index < len(children): + key_element = children[index] + if key_element.tag != 'key': + yield from _iter_duplicate_llsd_keys(key_element, path) + index += 1 + continue + + key = key_element.text or '' + value_element = children[index + 1] if index + 1 < len(children) else None + key_path = path + (key,) + if key in seen: + yield '.'.join(key_path) + else: + seen.add(key) + if value_element is not None: + yield from _iter_duplicate_llsd_keys(value_element, key_path) + index += 2 + elif element.tag in ('array', 'llsd'): + for child in list(element): + yield from _iter_duplicate_llsd_keys(child, path) + + +def _warn_duplicate_llsd_keys(xml_bytes, source): + try: + root = ET.fromstring(xml_bytes) + except ET.ParseError: + return + + for key_path in _iter_duplicate_llsd_keys(root): + logger.warning("File '%s' contains duplicate LLSD key '%s'; later definitions override earlier ones", + source, key_path) + + class ConfigurationDescription(common.Serialized): """ An autobuild configuration. @@ -217,6 +256,7 @@ def __load(self, path): if not autobuild_xml: logger.warning("Configuration file '%s' is empty" % self.path) return + _warn_duplicate_llsd_keys(autobuild_xml, self.path) try: saved_data = llsd.parse(autobuild_xml) except llsd.LLSDParseError: @@ -431,6 +471,7 @@ def __init__(self, path=None, stream=None, parsed_llsd=None, convert_platform=No elif stream: metadata_xml = stream.read() if metadata_xml: + _warn_duplicate_llsd_keys(metadata_xml, self.path or '') try: parsed_llsd = llsd.parse(metadata_xml) except llsd.LLSDParseError: diff --git a/tests/test_configfile.py b/tests/test_configfile.py index 2e812a2..14d4312 100644 --- a/tests/test_configfile.py +++ b/tests/test_configfile.py @@ -61,6 +61,61 @@ def test_configuration_inherit(self): # check that we fall back to the 32 bit version if no 64 bit is found assert reloaded.get_platform('darwin64').build_directory == 'darwin_build' + def test_configuration_warns_on_duplicate_llsd_keys(self): + tmp_file = self.get_tmp_file() + with open(tmp_file, 'wb') as f: + f.write(b""" + + + installables + + icu4c + + name + icu4c + platforms + + linux64 + + archive + + hash + 1111111111111111111111111111111111111111 + hash_algorithm + sha1 + url + https://example.com/first.tar.zst + + + linux64 + + archive + + hash + 2222222222222222222222222222222222222222 + hash_algorithm + sha1 + url + https://example.com/second.tar.zst + + + + + + type + autobuild + version + 1.3 + + +""") + + with self.assertLogs(configfile.logger, level='WARNING') as captured: + config = configfile.ConfigurationDescription(tmp_file) + + assert config.installables['icu4c'].platforms['linux64'].archive.url == 'https://example.com/second.tar.zst' + assert any("installables.icu4c.platforms.linux64" in message for message in captured.output) + def test_configuration_save_expanded(self): config = self.fake_config() # pretend to expand variables -- doesn't matter that there are no From d5126a6b076ddd48bf00eb5684219401dfeb9a50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Aug 2026 05:11:06 +0000 Subject: [PATCH 3/3] docs: clarify duplicate LLSD key detection --- autobuild/configfile.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/autobuild/configfile.py b/autobuild/configfile.py index 3032da6..0facb38 100644 --- a/autobuild/configfile.py +++ b/autobuild/configfile.py @@ -39,6 +39,13 @@ class NoVersionFileKeyError(common.AutobuildError): def _iter_duplicate_llsd_keys(element, path=()): + """ + Yield dotted LLSD map paths that are defined more than once in raw XML. + + We inspect the ElementTree instead of the parsed LLSD structure because + llsd.parse() has already applied the existing "last definition wins" + behavior by the time it returns a dict-like object. + """ if element.tag == 'map': seen = set() children = list(element) @@ -66,6 +73,13 @@ def _iter_duplicate_llsd_keys(element, path=()): def _warn_duplicate_llsd_keys(xml_bytes, source): + """ + Log duplicate LLSD map keys without changing Autobuild's merge semantics. + + This runs before parsing so package metadata can warn about repeated keys + introduced by merges while still allowing the later value to override the + earlier one, as Autobuild has historically done. + """ try: root = ET.fromstring(xml_bytes) except ET.ParseError: @@ -256,6 +270,8 @@ def __load(self, path): if not autobuild_xml: logger.warning("Configuration file '%s' is empty" % self.path) return + # Warn on repeated raw LLSD keys before llsd.parse() collapses them + # into a single dict entry. _warn_duplicate_llsd_keys(autobuild_xml, self.path) try: saved_data = llsd.parse(autobuild_xml)