Skip to content

build: rebuild an output another configuration left in the build directory - #7236

Merged
matz merged 3 commits into
mruby:masterfrom
takumin:build-flags-record
Aug 17, 2026
Merged

build: rebuild an output another configuration left in the build directory#7236
matz merged 3 commits into
mruby:masterfrom
takumin:build-flags-record

Conversation

@takumin

@takumin takumin commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

A build directory records what was built, not what it was built with, so two
configs that share a build directory and a build name share the objects, and the
second one silently gets the first one's compiler flags.

MRuby::Command::Compiler#all_flags (lib/mruby/build/command.rb:95-101 on
master) assembles the defines at the moment of the call and writes them nowhere
rake will read again. The .d file beside an object records header
dependencies only, and MRUBY_CONFIG is a dependency by path, whose mtime does
not move when another config takes the directory over. Every dependency the
object has says it is up to date, so nothing is compiled, and the binary carries
the defines of whichever config came first.

The name is what brings two configs together: CrossBuild#initialize generates
its minimal build under the name host, which is the name
build_config/default.rb uses too. Two runs of the same build name from
different build configs collide the same way.

Before

The same tree and the same command twice,
rake test MRUBY_CONFIG=build_config/no-float.rb, differing only in what was in
the build directory beforehand: nothing, or a default rake build.

own shared
compile lines for host 79 0
the mrbc it uses MRB_NO_FLOAT floats enabled
stops at the float literal irep load error
which is what #7230 merged what #7230 removed

With a directory of its own, the generated host build carries MRB_NO_FLOAT,
and its mrbc refuses the float literal where it is written:

$ MRUBY_BUILD_DIR=/tmp/nofloat-build rake -m test MRUBY_CONFIG=build_config/no-float.rb
...
        (79 compile lines for the generated `host` build)
...
test/t/array.rb:65: Not implemented: PM_FLOAT_NODE
test/t/array.rb:0:0: generator error, Not implemented: PM_FLOAT_NODE
rake aborted!

Over a directory a plain rake has populated, not one compile line for host.
The float-enabled mrbc of the earlier build is reused, writes IREP_TT_FLOAT
pool entries, and src/load.c refuses them on the MRB_NO_FLOAT target: the
pre-#7230 failure, on a tree that carries #7230.

$ rake -m                                          # populates build/host
$ rake test MRUBY_CONFIG=build_config/no-float.rb
...
 Output Directory: build/host                      # the only mention of build/host in the run
...
TEST for no-float
mrbtest - Embeddable Ruby Test

.........trace (most recent call last):
test/t/argumenterror.rb:34: irep load error (ScriptError)
rake aborted!

MRB_NO_FLOAT is the loud case, because the answer changes shape. MRB_INT32 /
MRB_INT64, MRB_GC_FIXED_ARENA and the boxing modes are the quiet ones, and a
measurement taken in a shared directory can report that a merged fix did not
land.

After

Every compile records the command line it ran with, beside its output, the way
the .d file records the headers it read:

$ cat build/host/src/array.o.flags
command: gcc
options: -MMD -c %{flags} -o "%{outfile}" "%{infile}"
flags: -std=gnu99 -g -O3 -Wall ... -DMRB_USE_BIGINT ... -I"/mruby/include"

The record is compared when the rule for that output is resolved. An output the
record does not answer for is removed, so rake builds it again. The shared
directory of the second run above:

master this PR
compile lines for host 0 79
says what changed nothing once per directory
stops at irep load error the float literal
the default config after it keeps the MRB_NO_FLOAT objects rebuilds them
$ rake -m                                          # populates build/host
$ rake test MRUBY_CONFIG=build_config/no-float.rb
/mruby/build/host: output here was built by another configuration, rebuilding it
  flags added: -DMRB_NO_FLOAT -DMRB_NO_GEMS
  flags removed: -DMRC_TARGET_MRUBY -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET ...
...
        (79 compile lines for `host`)
...
test/t/array.rb:65: Not implemented: PM_FLOAT_NODE
test/t/array.rb:0:0: generator error, Not implemented: PM_FLOAT_NODE
rake aborted!

Only a compile that succeeded writes a record, so a build that stops halfway
leaves nothing claiming a configuration its output was not built with: what it
did not reach is still the earlier output, still without a record that answers
for it, and the next run removes that too.

Two things this rests on:

  • The record is compared only through the rule that finds its source, which is
    the test rake itself applies before it uses a rule. Every compiler of a
    build defines a rule for the object names, differing in the extension of the
    source it looks for, and a compiler that does not build the output would
    otherwise judge it against flags no compile of it ever used.
  • tasks/presym.rake now hands every target its generated header directory
    before the walk that resolves rules begins. That walk reaches past the target
    it runs for, since a build's products lead to the objects of the mrbc build
    it generated, and a rule resolved for those objects has to see the include
    paths the compile will use.

What it costs

One directory, the default gembox:

master this PR
rake -m with nothing to do 0.60 s 0.62 s
records written 0 594 files, 335 KiB
build directory 184 MiB 184 MiB

The first rake in a directory built before this change rebuilds it once, there
being no record to compare it against, and says so:
output here has no record of what built it, rebuilding it.

An object built by a file task written by hand rather than by a rule is not
compared, the check hanging off the rule. mrbgems/mruby-compiler is the only
such place in the tree: its Prism objects choose their flags inside the task
body, and they carry no dependency on MRUBY_CONFIG either.

Verification

Under build_config/, each run twice:

config run again
default.rb 2123 assertions, 0 KO; bintest 111, 0 KO nothing compiled
ci/gcc-clang.rb 6 suites, 0 KO, 0 crash nothing compiled
host-cxx.rb builds nothing compiled
host-debug.rb builds nothing compiled
no-float.rb builds nothing compiled
minimal.rb builds nothing compiled
the config below builds nothing compiled

The last row is a config for the rules a build can define for one object name:
enable_cxx_exception, so that the cxx compiler defines rules for the object
names too, and two gems from examples/mrbgems, which are outside mrbgems/
and so use all four compilers. Overwriting the record of one core object and one
gem object in that build rebuilds those two and nothing else.

One directory, in this order:

outcome
rake -m test 2123 assertions, 0 KO; bintest 111, 0 KO
rake test with no-float.rb reports, rebuilds host, stops at the literal
rake -m test reports, rebuilds host, 2123 assertions, 0 KO
rake -m test nothing compiled

Environment

OS Linux 7.0.0-28-generic, x86_64
Compiler gcc 13.3.0
Ruby 4.0.6
rake 13.3.1

No C source changes, so .text is unaffected in every build.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ArcdfvwWWVJiJr1sWW4EQf

Summary by CodeRabbit

  • Bug Fixes
    • Improved build reliability by detecting when generated outputs use missing or outdated compiler settings.
    • Prevented stale or incompatible build artifacts from being reused.
    • Added clearer warnings showing which configuration fields were added or removed.
    • Improved dependency handling and presymbol processing across build targets and bundled components.

The walk that collects the preprocess targets resolves rules, and it
reaches past the target it runs for: the products of one build lead to
the objects of the `mrbc` build it generated, and those have rules of
their own. Resolved while the loop is still on the first target, they see
compilers that have not been handed their generated header directory yet,
though the compile that follows them has it.

Add the directory for every target in a pass of its own, before the walk
begins, so that a rule resolved for any target sees the include paths the
compile will use.
`define_rules` writes the same rule twice for every source extension,
once looking for the source beside the sources and once among the
generated files. The directory is all that differs.
@takumin
takumin requested a review from matz as a code owner August 17, 2026 08:29
@github-actions github-actions Bot added the build label Aug 17, 2026
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c24b9ad2-b15a-4ef1-8a63-61ec76331e10

📥 Commits

Reviewing files that changed from the base of the PR and between 46e6573 and a0e25e8.

📒 Files selected for processing (1)
  • lib/mruby/build/command.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/mruby/build/command.rb

Included review availability: Your plan includes up to 8 reviews per rolling hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

The build system records compiler flags for each output, detects outputs from other configurations, reports mismatches once per build, and removes stale outputs. Presymbol include paths are initialized before presymbol dependency resolution.

Changes

Compiler output configuration tracking

Layer / File(s) Summary
Flags-change reporting
lib/mruby/build.rb
MRuby::Build now synchronizes mismatch reporting and reports missing, added, or removed configuration fields once per build.
Presymbol include-path setup
tasks/presym.rake
Target and gem include paths are initialized before presymbol dependency processing.
Compiler flags and output validation
lib/mruby/build/command.rb
Compiler flags are centralized, dependencies use the resolved source path, successful outputs store command configuration, and mismatched outputs are removed.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to a0e25

The build now records compiler flags and rebuilds outputs when configurations differ, preventing stale objects from being reused across configurations; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Compiler
  participant DependencyResolver
  participant FlagsRecord
  participant MRubyBuild
  Compiler->>Compiler: construct compile_flags
  Compiler->>DependencyResolver: resolve dependencies for source
  Compiler->>FlagsRecord: read output configuration
  Compiler->>MRubyBuild: report_flags_change(recorded, current)
  Compiler->>Compiler: remove mismatched output
  Compiler->>FlagsRecord: write successful output configuration
Loading

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: rebuilding outputs left by another configuration in the shared build directory.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ctory

A build directory records what was built, not what it was built with, so
two configs that share `MRUBY_BUILD_DIR` and a build name hand each other
their objects. `MRuby::Command::Compiler#all_flags` assembles the defines
when the compile runs and writes them nowhere; the `.d` file beside an
object carries header dependencies only; and the config file is a
dependency by path, so its mtime does not move when another config takes
the directory over. The object is up to date against every dependency it
has, and the binary silently carries the defines of whichever config came
first.

`build_config/no-float.rb` over a directory a default `rake` has
populated is the case that shows, because `CrossBuild#initialize`
generates its minimal build under the name `host`, which is the name
`build_config/default.rb` uses too:

```console
$ rake test MRUBY_CONFIG=build_config/no-float.rb
...
test/t/argumenterror.rb:34: irep load error (ScriptError)
```

Not one compile line for `host`. The float-enabled `mrbc` of the earlier
build writes `IREP_TT_FLOAT` pool entries and `src/load.c` refuses them on
the `MRB_NO_FLOAT` target, the failure a build directory of its own no
longer has.

Record the command line each compile ran with, beside its output the way
the `.d` file records the headers it read, and compare it when the rule
for that output is resolved. An output the record does not answer for, or
that has no record at all, is removed, so that rake builds it again. Only
a compile that succeeded writes a record, so a build that stops halfway
leaves nothing claiming a configuration its output was not built with.
The directory says once what changed:

```console
$ rake test MRUBY_CONFIG=build_config/no-float.rb
/tmp/build/host: output here was built by another configuration, rebuilding it
  flags added: -DMRB_NO_FLOAT -DMRB_NO_GEMS
  flags removed: -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DMRB_USE_RATIONAL ...
```

The record is compared only through the rule that finds its source, the
same test rake applies before it uses a rule. Every compiler of a build
defines a rule for the object names, differing in the extension of the
source it looks for, and the ones that do not build the output would
otherwise judge it against flags no compile of it ever used.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants