Early Found a Conan Regression in a 134-File Release
One changed predicate turned a valid no-op replacement into a build failure. Early identified it among nine findings.
Technical Replay | Targeted historical replay
A file contained the search pattern. Conan said it did not.
Conan, the open-source C/C++ package manager maintained by JFrog, changed replace_in_file() while adding regular expression support. The helper stopped asking whether a pattern was present and started asking whether the replacement changed the file. When a valid replacement intentionally left the content unchanged, Conan treated it as a missing pattern and raised a build-stopping exception.
We later selected this known regression window and ran a targeted historical replay from Conan 2.30.0 to 2.31.0. Early analyzed 41 of the release comparison’s 134 changed files and returned nine findings. One was identified as a Regression, and it was this exact defect.
Independent replay of a public Conan release. Conan and JFrog did not participate.
Read the concise Conan replace_in_file Regression Case File for the release outcome, failure mechanism, and Early result at a glance.
How replace_in_file Failed
The public replace_in_file() helper lets Conan recipes find and replace text in files during a build. By default, the helper raises an error when it cannot find the requested pattern.
That contract makes sense. A missing pattern may mean that an upstream file changed and the recipe is no longer modifying what its author expected.
The problem in 2.31.0 was that Conan could raise the same error when the pattern was present.
Consider a file that contains /MT and this replacement:
replace_in_file(conanfile, path, "/MT ", "/MT ")
The operation is intentionally a no-op. The pattern exists, so the documented strict-mode condition is satisfied. Conan 2.30.0 accepted it. Conan 2.31.0 raised:
ConanException: replace_in_file didn't find pattern '/MT ' in '<path>' file.
The message was not merely unhelpful. It asserted the opposite of what was in the file.
Release Timeline
The introducing change came from PR #20194, which added regular expression support to replace_in_file(). Three maintainers approved the pull request. Reviewers left seven comments, and the change passed 29 automated checks across Linux, macOS, Windows, and Python 3.7 through 3.14. Four additional checks were skipped and none failed.
The change merged on July 23, 2026 at 13:47 UTC. Conan 2.31.0 shipped less than two hours later at 15:25 UTC.
The fix merged the next morning and shipped in 2.31.1 at 11:54 UTC. The released regression window lasted about 20 hours and 29 minutes.
The fix pull request opened about five hours before public issue #20212 appeared. The issue was filed at 10:38 UTC, and the fix merged three minutes later. The public record does not establish how the maintainers first discovered the defect, so this replay does not assign that credit.
The regression remained in the released package for about 20 hours and 29 minutes.
The Broken Predicate
Before 2.31.0, the helper’s logic matched its public contract. It checked whether the search value was present in the file. If the pattern existed, the replacement was valid even when the resulting content happened to be identical.
The regular expression change introduced a different test:
if regex:
new_content = re.sub(search, replace, content, flags=flags)
else:
new_content = content.replace(search, replace)
if new_content == content:
raise ConanException("didn't find pattern")
This code asks whether the file changed. That is not the same question as whether the pattern was found.
Most replacements make the two conditions look equivalent. A present pattern changes the file, while an absent pattern does not. A no-op replacement exposes the difference. The search succeeds, but the output equals the input.
The new predicate measured content difference instead of pattern presence.
The 2.31.1 fix restored the correct distinction. The literal path checks search in content. The regular expression path uses re.subn() and checks the match count. Regular expression support remained in place. The fix repaired foundness rather than reverting the feature.
The Historical Replay
This was not a blind trial. Researchers deliberately selected the 2.30.0 to 2.31.0 comparison because it contained a known regression. The ground truth was documented before the Early output was reviewed.
Early analyzed only the configured source-to-target comparison. It was not given the later issue, fix, or expected answer. The analysis had no web, GitHub, retrieval, or other external access. Project configuration did not identify the target function or failure, and the product output was captured before researchers compared it with the frozen ground truth.
The release comparison contained 134 changed files. Early’s filtering excluded 93 files before analysis, leaving 41 files for detailed review.
The run returned nine findings:
| Verdict | Count | Independent review |
|---|---|---|
| Regression | 1 | The real replace_in_file() defect |
| Expected | 8 | Not all independently verified as harmless |
There were no unmatched Regression findings. The precision statement is intentionally narrow. The one Regression finding was a true positive. We did not independently establish that every Expected finding was harmless.
What Early Found
Early’s finding was titled “replace_in_file misreports no-op replace as not-found.”
It identified the exact function in conan/tools/files/files.py, the difference between pattern presence and resulting-content equality, and the user-visible effect. A caller could supply an ordinary no-op replacement, succeed in the previous release, and receive a false hard failure in 2.31.0.
The finding also proposed the decisive verification: create a file containing the search text, replace that text with itself under strict mode, and confirm that the operation succeeds rather than reporting the pattern as missing.


Actual Early product finding from the Conan historical replay, shown as one of nine findings.
Independent review matched the finding to the upstream defect on five points: location, mechanism, user effect, classification, and reproduction.
Verification and Limits
We ran a small reproduction against four published Conan versions. It performs one replacement that changes content as a positive control, then tests the no-op case that separates pattern presence from content difference.
| Version | Result | Observed behavior |
|---|---|---|
| 2.29.0 | Pass | Present no-op replacement succeeded |
| 2.30.0 | Pass | Present no-op replacement succeeded |
| 2.31.0 | Fail | False “pattern not found” exception |
| 2.31.1 | Pass | Present no-op replacement succeeded |
The pass, pass, fail, pass boundary independently confirms the introducing and fixed releases. The script runs against published PyPI packages. After installation, verification is a local file operation and needs no Conan remote, profile, Artifactory, ConanCenter, project build, or upstream test suite.
The trigger was also reachable in real recipe code. In a dated snapshot of ConanCenter Index, our corpus review found replace_in_file() in 418 recipes. Roughly 396 used the default strict behavior and were structurally exposed to this class of failure. Two OpenSSL recipes contained a runtime-driven no-op path that could trigger the defect when the corresponding flag was present.
Those corpus numbers describe potential code paths, not a measured count of failed builds. We did not execute a Windows OpenSSL build. The corpus review helps establish potential blast radius, but it was not needed to detect the contract violation, which was visible inside Conan’s own public helper and docstring.
There is another important limit. Two earlier internal replays localized the same behavior but did not classify it as a Regression. The run reported here did. We preserve both outcomes in our benchmark record.
The latest result does not prove deterministic detection across every run, mode, repository, or regression. It shows something narrower and still useful. In this targeted historical replay, Early identified the exact defect as the only Regression finding among nine total findings.
The Engineering Lesson
The change was small, reviewed by three maintainers, and covered by a broad multi-platform CI matrix. The team also repaired it quickly. This is not a story about careless engineering.
It is a story about a predicate changing meaning.
The error path said “pattern not found,” but the new condition measured “file did not change.” Those propositions overlap in common cases and diverge at an edge that is easy to miss. The missing test cell was not complex:
| Pattern present | Content changes | Expected result |
|---|---|---|
| Yes | Yes | Success |
| No | No | Not found |
| Yes | No | Success |
That third row is the entire regression.
The practical review question is direct: does the predicate establish the condition that the error message and public contract claim? When those meanings drift apart, ordinary input can turn a useful guardrail into a false failure.
Request the Replay
The case materials include the release history, complete product output, and the four-version reproduction. They are available by request.
With access, you can:
- Inspect the code and history between Conan 2.30.0 and 2.31.0.
- Compare Early’s finding with the introducing change and later upstream fix.
- Run the reproduction and verify the pass, pass, fail, pass release boundary.
Contact us to request access to the Conan replay materials.
Sources and Method
Primary public sources include the introducing pull request, the introducing release comparison, the fixing pull request, the fixed release comparison, and the public issue.
Human verification compared the stored Early output with the release source, project review and CI records, the executed four-version reproduction, and the later upstream fix.
