From 3da9a4ff8d2a7733bab0e8b392e4fc32896afae3 Mon Sep 17 00:00:00 2001 From: Dnreikronos Date: Tue, 2 Jun 2026 17:44:54 -0300 Subject: [PATCH 1/3] Support dotted table keys when parsing bootstrap.toml bootstrap.py uses a small hand-written reader to find the stage0 cargo and rustc in bootstrap.toml before the real TOML parser is available. It only recognized `[table]` headers, so dotted keys such as `build.cargo = "..."` -- the form now used in bootstrap.example.toml -- were silently ignored. Match an optional dotted prefix on each key and treat that prefix as the table the key belongs to, mirroring how configure.py strips the `section.` part when writing the config. --- src/bootstrap/bootstrap.py | 25 ++++++++++++++++++++++--- src/bootstrap/bootstrap_test.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/bootstrap/bootstrap.py b/src/bootstrap/bootstrap.py index 563792868f5d2..063a44a3e737f 100644 --- a/src/bootstrap/bootstrap.py +++ b/src/bootstrap/bootstrap.py @@ -926,6 +926,15 @@ def get_toml(self, key, section=None): >>> rb.get_toml('key', 'c') is None True + A dotted key is treated as belonging to the table named by its prefix, + so the section must match for the key to be found: + + >>> rb.config_toml = 'build.cargo = "/path/to/cargo"' + >>> rb.get_toml('cargo', 'build') + '/path/to/cargo' + >>> rb.get_toml('cargo', 'other') is None + True + >>> rb.config_toml = 'key1 = true' >>> rb.get_toml("key1") 'true' @@ -940,10 +949,20 @@ def get_toml_static(config_toml, key, section=None): if section_match is not None: cur_section = section_match.group(1) - match = re.match(r"^{}\s*=(.*)$".format(key), line) + # Match the key, optionally preceded by a dotted-table prefix such as + # the `build.` in `build.cargo = "..."`. When present, that prefix + # names the table the key belongs to, mirroring how configure.py + # strips the `section.` part when writing bootstrap.toml. This is not + # a full TOML parser, only the subset bootstrap.toml uses. + match = re.match( + r"^\s*(?:([\w.-]+)\.)?{}\s*=(.*)$".format(re.escape(key)), line + ) if match is not None: - value = match.group(1) - if section is None or section == cur_section: + line_section = ( + match.group(1) if match.group(1) is not None else cur_section + ) + value = match.group(2) + if section is None or section == line_section: return RustBuild.get_string(value) or value.strip() return None diff --git a/src/bootstrap/bootstrap_test.py b/src/bootstrap/bootstrap_test.py index b9cd0d7aa34a7..5d4ca2a412002 100644 --- a/src/bootstrap/bootstrap_test.py +++ b/src/bootstrap/bootstrap_test.py @@ -190,6 +190,35 @@ def test_set_codegen_backends(self): self.assertNotEqual(build.config_toml.find("codegen-backends = ['llvm']"), -1) +class GetTomlDottedKeys(unittest.TestCase): + """Test that get_toml understands the dotted-table key syntax that a + hand-written bootstrap.toml may use (e.g. `build.cargo = "..."`), and that + it checks the table a key belongs to. See issue #156948.""" + + def parse(self, config_toml): + return bootstrap.RustBuild(config_toml=config_toml, args=bootstrap.FakeArgs()) + + def test_dotted_key(self): + build = self.parse('build.cargo = "/path/to/cargo"') + self.assertEqual(build.get_toml("cargo", "build"), "/path/to/cargo") + + def test_dotted_key_matches_section_form(self): + dotted = self.parse('build.cargo = "/path/to/cargo"') + section = self.parse('[build]\ncargo = "/path/to/cargo"') + self.assertEqual( + dotted.get_toml("cargo", "build"), section.get_toml("cargo", "build") + ) + + def test_dotted_key_wrong_section(self): + # A `cargo` key in some other table must not be picked up for `build`. + build = self.parse('[foo]\ncargo = "false"') + self.assertIsNone(build.get_toml("cargo", "build")) + + def test_dotted_target_key(self): + build = self.parse('target.x86_64-unknown-linux-gnu.cc = "gcc"') + self.assertEqual(build.get_toml("cc", "target.x86_64-unknown-linux-gnu"), "gcc") + + class BuildBootstrap(unittest.TestCase): """Test that we generate the appropriate arguments when building bootstrap""" From 27142a51d1789cfc26607a9db7079cb6282e5fe3 Mon Sep 17 00:00:00 2001 From: Dnreikronos Date: Tue, 2 Jun 2026 17:45:37 -0300 Subject: [PATCH 2/3] Look up cargo and rustc in the build table program_config searched every table for the `cargo` and `rustc` keys, so a key placed in an unrelated table was picked up while the real config parser only reads them from `[build]`. Scope the lookup to the build table so the Python and Rust sides agree. --- src/bootstrap/bootstrap.py | 7 +++++-- src/bootstrap/bootstrap_test.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/bootstrap/bootstrap.py b/src/bootstrap/bootstrap.py index 063a44a3e737f..dee9f2243065d 100644 --- a/src/bootstrap/bootstrap.py +++ b/src/bootstrap/bootstrap.py @@ -978,7 +978,10 @@ def program_config(self, program): """Return config path for the given program at the given stage >>> rb = RustBuild() - >>> rb.config_toml = 'rustc = "rustc"\\n' + >>> rb.config_toml = 'build.rustc = "rustc"\\n' + >>> rb.program_config('rustc') + 'rustc' + >>> rb.config_toml = '[build]\\nrustc = "rustc"\\n' >>> rb.program_config('rustc') 'rustc' >>> rb.config_toml = '' @@ -987,7 +990,7 @@ def program_config(self, program): ... "bin", "cargo") True """ - config = self.get_toml(program) + config = self.get_toml(program, "build") if config: return os.path.expanduser(config) return os.path.join(self.bin_root(), "bin", "{}{}".format(program, EXE_SUFFIX)) diff --git a/src/bootstrap/bootstrap_test.py b/src/bootstrap/bootstrap_test.py index 5d4ca2a412002..4c171a58534c9 100644 --- a/src/bootstrap/bootstrap_test.py +++ b/src/bootstrap/bootstrap_test.py @@ -214,6 +214,25 @@ def test_dotted_key_wrong_section(self): build = self.parse('[foo]\ncargo = "false"') self.assertIsNone(build.get_toml("cargo", "build")) + def test_program_config_requires_build_table(self): + # A cargo/rustc key outside [build] must be ignored; program_config + # falls back to the stage0 default path rather than the misplaced value. + wrong = self.parse('[foo]\ncargo = "/wrong/cargo"') + cargo_default = os.path.join( + wrong.bin_root(), "bin", "cargo" + bootstrap.EXE_SUFFIX + ) + self.assertEqual(wrong.cargo(), cargo_default) + + wrong_rustc = self.parse('[foo]\nrustc = "/wrong/rustc"') + rustc_default = os.path.join( + wrong_rustc.bin_root(), "bin", "rustc" + bootstrap.EXE_SUFFIX + ) + self.assertEqual(wrong_rustc.rustc(), rustc_default) + + # A dotted key in the build table is honored. + right = self.parse('build.cargo = "/path/to/cargo"') + self.assertEqual(right.cargo(), "/path/to/cargo") + def test_dotted_target_key(self): build = self.parse('target.x86_64-unknown-linux-gnu.cc = "gcc"') self.assertEqual(build.get_toml("cc", "target.x86_64-unknown-linux-gnu"), "gcc") From 7769622cb9f36230977a811af3b5caa8aee8cac8 Mon Sep 17 00:00:00 2001 From: Dnreikronos Date: Wed, 3 Jun 2026 16:01:34 -0300 Subject: [PATCH 3/3] Resolve dotted table keys relative to their enclosing section A dotted key such as `x86_64-unknown-linux-gnu.cc` is relative to the table it appears in, so its prefix names a sub-table of the current `[section]` rather than a table of its own. Compose the prefix with `cur_section` instead of replacing it. This fixes two cases: a dotted key nested under a `[section]` header is now found by its full path, and a dotted prefix that happens to match another table's name (e.g. `build.cargo` under `[foo]`) is no longer misattributed to that table. --- src/bootstrap/bootstrap.py | 37 ++++++++++++++++++++++++--------- src/bootstrap/bootstrap_test.py | 19 +++++++++++++++++ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/bootstrap/bootstrap.py b/src/bootstrap/bootstrap.py index dee9f2243065d..cb82eac8e89dd 100644 --- a/src/bootstrap/bootstrap.py +++ b/src/bootstrap/bootstrap.py @@ -926,8 +926,8 @@ def get_toml(self, key, section=None): >>> rb.get_toml('key', 'c') is None True - A dotted key is treated as belonging to the table named by its prefix, - so the section must match for the key to be found: + A dotted key names a table relative to its enclosing section, so the + full table path must match for the key to be found: >>> rb.config_toml = 'build.cargo = "/path/to/cargo"' >>> rb.get_toml('cargo', 'build') @@ -935,6 +935,19 @@ def get_toml(self, key, section=None): >>> rb.get_toml('cargo', 'other') is None True + A dotted key inside a section composes with that section's name: + + >>> rb.config_toml = '[target]\\nx86_64-unknown-linux-gnu.cc = "gcc"' + >>> rb.get_toml('cc', 'target.x86_64-unknown-linux-gnu') + 'gcc' + + and a dotted prefix that collides with another table's name is not + mistaken for that table: + + >>> rb.config_toml = '[foo]\\nbuild.cargo = "false"' + >>> rb.get_toml('cargo', 'build') is None + True + >>> rb.config_toml = 'key1 = true' >>> rb.get_toml("key1") 'true' @@ -949,18 +962,22 @@ def get_toml_static(config_toml, key, section=None): if section_match is not None: cur_section = section_match.group(1) - # Match the key, optionally preceded by a dotted-table prefix such as - # the `build.` in `build.cargo = "..."`. When present, that prefix - # names the table the key belongs to, mirroring how configure.py - # strips the `section.` part when writing bootstrap.toml. This is not - # a full TOML parser, only the subset bootstrap.toml uses. + # Match the key, optionally preceded by a dotted-table prefix (the + # `build.` in `build.cargo`), which names a table relative to the + # current `[section]` and is appended to `cur_section`. This is a + # subset parser, not full TOML: quoted names (e.g. the `'a.b'` that + # configure.py emits for dotted targets) are not matched here. match = re.match( r"^\s*(?:([\w.-]+)\.)?{}\s*=(.*)$".format(re.escape(key)), line ) if match is not None: - line_section = ( - match.group(1) if match.group(1) is not None else cur_section - ) + prefix = match.group(1) + if prefix is None: + line_section = cur_section + elif cur_section is None: + line_section = prefix + else: + line_section = "{}.{}".format(cur_section, prefix) value = match.group(2) if section is None or section == line_section: return RustBuild.get_string(value) or value.strip() diff --git a/src/bootstrap/bootstrap_test.py b/src/bootstrap/bootstrap_test.py index 4c171a58534c9..163191aa01f32 100644 --- a/src/bootstrap/bootstrap_test.py +++ b/src/bootstrap/bootstrap_test.py @@ -237,6 +237,25 @@ def test_dotted_target_key(self): build = self.parse('target.x86_64-unknown-linux-gnu.cc = "gcc"') self.assertEqual(build.get_toml("cc", "target.x86_64-unknown-linux-gnu"), "gcc") + def test_dotted_key_inside_section(self): + # A dotted key nested under a `[section]` header composes with that + # section's name; it is equivalent to the fully top-level dotted form. + nested = self.parse('[target]\nx86_64-unknown-linux-gnu.cc = "gcc"') + self.assertEqual( + nested.get_toml("cc", "target.x86_64-unknown-linux-gnu"), "gcc" + ) + top_level = self.parse('target.x86_64-unknown-linux-gnu.cc = "gcc"') + self.assertEqual( + nested.get_toml("cc", "target.x86_64-unknown-linux-gnu"), + top_level.get_toml("cc", "target.x86_64-unknown-linux-gnu"), + ) + + def test_dotted_prefix_does_not_alias_other_section(self): + # `build.cargo` inside `[foo]` is `foo.build.cargo`, not the top-level + # `[build]` table, so it must not be returned for section "build". + build = self.parse('[foo]\nbuild.cargo = "false"') + self.assertIsNone(build.get_toml("cargo", "build")) + class BuildBootstrap(unittest.TestCase): """Test that we generate the appropriate arguments when building bootstrap"""