Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor git change detection in bootstrap #138591

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

Kobzol
Copy link
Contributor

@Kobzol Kobzol commented Mar 17, 2025

While working on #138395, I finally found the courage to delve into the insides of git path change detection in bootstrap, which is used (amongst other things) to detect if we should rebuilt od download [llvm|rustc|gcc]. I found it a bit hard to understand, and given that this code was historically quite fragile, I thought that it would be better to rebuild it from scratch.

The previous approach had a bunch of limitations:

  • It separated the computation of "are there local changes?" and "what upstream SHA should we use?" even though these two things are intertwined.
  • It used hacks to work around what happens on CI.
  • It had special cases for CI scattered throughout the codebase, rather than centralized in one place.
  • It wasn't documented enough and didn't have tests for the git behavior.

The current approach should hopefully resolve all of that. I implemented a single entrypoint called check_path_modifications (naming bikeshed pending, half of the time I spend on this PR was thinking about names, as it's quite tricky here..) that explicitly receives a mode of operation (in CI or outside CI), and accordingly figures out that upstream SHA that we should use for downloading artifacts and it also figures out if there are any local changes. Users of this function can then use this unified output to implement download-ci-X and other functionality. Notably, this change detection no longer uses git merge-base, which makes it easier to use and doesn't require setting up remotes.

I also added a bunch of integration tests that literally spawn a git repository on disk and then check that the function can deal with various situations (PR CI, auto/try CI, local builds).

After I built this inner layer, I used it for downloading GCC, LLVM and rustc. The latter two (and especially rustc) were using the last_modified_commit function before, but in all cases but one this function was actually only used to check if there are any local changes, which was IMO confusing. The LLVM handling would deserve a bit of refactoring, but that's a larger change that can be done as a follow-up.

I hope that the implementation is now clear and easy to understand, so that in combination with the tests we can have more confidence that it does what we want. I tried to include a lot of documentation in the code, so I won't be repeating the actual implementation details here, if there are any questions, I'll add the answers to the documentation too :)

The new approach explicitly supports three scenarios:

  • Running on PR CI, where we have one upstream bors parent commit and one PR merge commit made by GitHub.
  • Running on try/auto CI, where we have one upstream bors parent commit and one PR merge commit made by bors.
  • Running locally, where we assume that we have at least one upstream bors parent commit in our git history.

I removed the handling of upstreams on CI, as I think that it shouldn't be needed and I considered it to be a hack. However, it's possible that there are other use-cases that I haven't considered, so I want to ask around if people have other situations than the three use-cases described above. If there are other such use-cases, I would like to include them in the new centralized implementation and add them to the git test suite, rather than going back to the old ways :)

In particular, the code before relied on git merge-base, but I don't see why we can't just lookup the most recent bors commit and assume that is a merge commit that is also upstream? I might be running into Chesterton's Fence here :)

CC @pietroalbini To make sure that this won't break downstream users of Rust's CI.

Best reviewed commit by commit.

Companion PRs:

r? @onur-ozkan

try-job: x86_64-gnu-aux

@rustbot rustbot added A-compiletest Area: The compiletest test runner A-testsuite Area: The testsuite used to check the correctness of rustc S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) T-infra Relevant to the infrastructure team, which will review and decide on the PR/issue. labels Mar 17, 2025
@rustbot
Copy link
Collaborator

rustbot commented Mar 17, 2025

This PR changes how GCC is built. Consider updating src/bootstrap/download-ci-gcc-stamp.

These commits modify the Cargo.lock file. Unintentional changes to Cargo.lock can be introduced when switching branches and rebasing PRs.

If this was unintentional then you should revert the changes before this PR is merged.
Otherwise, you can ignore this comment.

This PR changes how LLVM is built. Consider updating src/bootstrap/download-ci-llvm-stamp.

Some changes occurred in src/tools/compiletest

cc @jieyouxu

This PR modifies src/bootstrap/src/core/config.

If appropriate, please update CONFIG_CHANGE_HISTORY in src/bootstrap/src/utils/change_tracker.rs.

@rustbot rustbot added the T-release Relevant to the release subteam, which will review and decide on the PR/issue. label Mar 17, 2025
@pietroalbini
Copy link
Member

LGTM on the Ferrocene side. There is nothing here that would break our downstream usage.

On the Rust side, I recommend opening this PR against stable and beta too, and running a full bors try on it. We had issues in past releases where changes to this code would unexpectedly break stable or beta CI, and I'd love for those to be catched before merging.

@Kobzol
Copy link
Contributor Author

Kobzol commented Mar 17, 2025

Yes, I planned to do that, it's a good idea. Actually, I can try that right away.

bors added a commit to rust-lang-ci/rust that referenced this pull request Mar 17, 2025
[do not merge] beta test for git change detection (rust-lang#138591)

Opening to test CI/bootstrap changes.

r? `@ghost`

try-job: x86_64-gnu-stable
try-job: x86_64-gnu
try-job: x86_64-gnu-llvm-19-1
try-job: dist-x86_64-linux
bors added a commit to rust-lang-ci/rust that referenced this pull request Mar 17, 2025
[do not merge] beta test for git change detection (rust-lang#138591)

Opening to test CI/bootstrap changes from rust-lang#138591.

r? `@ghost`

try-job: x86_64-gnu-stable
try-job: x86_64-gnu
try-job: x86_64-gnu-llvm-19-1
try-job: dist-x86_64-linux
bors added a commit to rust-lang-ci/rust that referenced this pull request Mar 17, 2025
[do not merge] beta test for git change detection (rust-lang#138591)

Opening to test CI/bootstrap changes from rust-lang#138591.

r? `@ghost`

try-job: x86_64-gnu-stable
try-job: x86_64-gnu
try-job: x86_64-gnu-llvm-19-1
try-job: dist-x86_64-linux
@jieyouxu jieyouxu self-assigned this Mar 17, 2025
@onur-ozkan
Copy link
Member

The changes look good, but I am not sure if they will break the if-unchanged tests and logic in the following cases:

  • PR that is supposed to use ci-rustc and ci-llvm
  • PR that is not supposed to use ci-rustc and ci-llvm
  • Testing the above cases on both stable and beta PRs

I think it's safer to make sure these won't be a problem before merging this.

@jieyouxu jieyouxu removed their assignment Mar 18, 2025
bors added a commit to rust-lang-ci/rust that referenced this pull request Mar 18, 2025
[do not merge] beta test for git change detection (rust-lang#138591)

Opening to test CI/bootstrap changes from rust-lang#138591.

r? `@ghost`

try-job: x86_64-gnu-aux
bors added a commit to rust-lang-ci/rust that referenced this pull request Mar 18, 2025
[do not merge] beta test for git change detection (rust-lang#138591)

Opening to test CI/bootstrap changes from rust-lang#138591.

r? `@ghost`

try-job: x86_64-gnu-aux
@Kobzol
Copy link
Contributor Author

Kobzol commented Mar 18, 2025

@bors try

bors added a commit to rust-lang-ci/rust that referenced this pull request Mar 18, 2025
Refactor git change detection in bootstrap

While working on rust-lang#138395, I finally found the courage to delve into the insides of git path change detection in bootstrap, which is used (amongst other things) to detect if we should rebuilt od download `[llvm|rustc|gcc]`. I found it a bit hard to understand, and given that this code was historically quite fragile, I thought that it would be better to rebuild it from scratch.

The previous approach had a bunch of limitations:
- It separated the computation of "are there local changes?" and "what upstream SHA should we use?" even though these two things are intertwined.
- It used hacks to work around what happens on CI.
- It had special cases for CI scattered throughout the codebase, rather than centralized in one place.
- It wasn't documented enough and didn't have tests for the git behavior.

The current approach should hopefully resolve all of that. I implemented a single entrypoint called `check_path_modifications` (naming bikeshed pending, half of the time I spend on this PR was thinking about names, as it's quite tricky here..) that explicitly receives a mode of operation (in CI or outside CI), and accordingly figures out that upstream SHA that we should use for downloading artifacts and it also figures out if there are any local changes. Users of this function can then use this unified output to implement `download-ci-X` and other functionality.

I also added a bunch of integration tests that literally spawn a git repository on disk and then check that the function can deal with various situations (PR CI, auto/try CI, local builds). The tests are super fast and run in parallel, as they are currently in `build_helper` and not in `bootstrap`.

After I built this inner layer, I used it for downloading GCC, LLVM and rustc. The latter two (and especially rustc) were using the `last_modified_commit` function before, but in all cases but one this function was actually only used to check if there are any local changes, which was IMO confusing. The LLVM handling would deserve a bit of refactoring, but that's a larger change that can be done as a follow-up.

In the future we could cache the results of `check_path_modifications` to reduce the number of git invocations, but I don't think that it should be excessive even now.

I hope that the implementation is now clear and easy to understand, so that in combination with the tests we can have more confidence that it does what we want. I tried to include a lot of documentation in the code, so I won't be repeating the actual implementation details here, if there are any questions, I'll add the answers to the documentation too :)

The new approach explicitly supports three scenarios:
- Running on PR CI, where we have one upstream bors parent commit and one PR merge commit made by GitHub.
- Running on try/auto CI, where we have one upstream bors parent commit and one PR merge commit made by bors.
- Running locally, where we assume that we have at least one upstream bors parent commit in our git history.

I removed the handling of upstreams on CI, as I think that it shouldn't be needed and I considered it to be a hack. However, it's possible that there are other use-cases that I haven't considered, so I want to ask around if people have other situations than the three use-cases described above. If there are other such use-cases, I would like to include them in the new centralized implementation and add them to the git test suite, rather than going back to the old ways :)

In particular, the code before relied on `git merge-base`, but I don't see why we can't just lookup the most recent bors commit and assume that is a merge commit that is also upstream? I might be running into Chesterton's Fence here :)

CC `@pietroalbini` To make sure that this won't break downstream users of Rust's CI.

Best reviewed commit by commit.

Companion PRs:
- For testing beta: rust-lang#138597

r? `@onur-ozkan`

try-job: x86_64-gnu-aux
@bors
Copy link
Contributor

bors commented Mar 18, 2025

⌛ Trying commit afe1f99 with merge 27ee8fc...

@Kobzol
Copy link
Contributor Author

Kobzol commented Mar 18, 2025

Did a bunch of follow-up clean-ups. Let me know if you want me to split this into multiple PRs! :)

@Kobzol Kobzol force-pushed the git-ci branch 2 times, most recently from 63be8ba to e1fe7f2 Compare March 19, 2025 10:07
@jieyouxu
Copy link
Member

I can co-review this, but not today (probably going to be tmrw or a bit later this week). Still looking at the stage 0 redesign PR.

@RalfJung
Copy link
Member

RalfJung commented Mar 24, 2025

Do we have a good idea for why CI needs to be special-cased?

I think part of the reason is that CI has a sparse checkout, which makes walking the git history largely meaningless. So what I think we should do there is just get the latest bors commit irrespective of paths. In fact I think that is already what is happening anyway, it's just more confusing since the code looks like it finds the last commit where the paths changed (but on CI, that's not what happens).

Copy link
Member

@RalfJung RalfJung left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I think I found the comment where this is discussed, and that comment sounds good. :) I didn't check the code.

/// were not modified upstream in the meantime. In that case we would be redownloading CI
/// artifacts unnecessarily.
///
/// - In CI, we always fetch only a single parent merge commit, so we do not have access
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// - In CI, we always fetch only a single parent merge commit, so we do not have access
/// - In CI, we use a sparse checkout. We fetch only a single parent merge commit, so we do not have access

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should call it a sparse checkout, tbh. Sparse checkout is a separate git thing that we are not using on CI. What you probably meant is a shallow clone, but that's also not accurate, because we actually checkout the last two commits. I'm not sure if there's a common term for that :)

Copy link
Member

@RalfJung RalfJung Mar 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, "shallow clone" is what I meant, yes. And I would say that is accurate, the clone is shallow. It has depth 1. That's not the same as depth 0, but it's still a shallow clone. "shallow" just means "not the entire history has been fetched", it doesn't mean "depth 0".

Suggested change
/// - In CI, we always fetch only a single parent merge commit, so we do not have access
/// - In CI, we use a shallow clone of depth 1, i.e., we fetch only a single parent commit (which will be the most recent bors merge commit) and do not have access

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, fair enough, "shallow" as a term is more general I suppose. This might be a bit confusing if someone takes a look at our CI fetch code, which confusingly uses depth 2 (because depth 0 here means "fetch everything"). But that's a small thing. Used your text to hopefully clarify the comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the clone has depth 2 we should say that, "1" was just a guess on my side. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, yes, the clone depth is indeed 2. I thought that shallow clone is --depth=0, but I remembered that wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"2-depth checkout"

Kobzol added 12 commits March 24, 2025 09:09
…hs have been modified locally

Also adds several git tests to make sure that the behavior works in common cases (PR CI, auto CI, local usage).
And get rid of `get_closest_merge_commit`.
It shouldn't really happen, but if it does, at least we will have an explicit record of it.
The new git tests should be enough to check this scenario. We should ideally not be creating dummy commits on CI.
@Kobzol
Copy link
Contributor Author

Kobzol commented Mar 24, 2025

Rebased and pushed two changes based on review.

@rust-log-analyzer

This comment has been minimized.

@Kobzol Kobzol force-pushed the git-ci branch 3 times, most recently from 95b28c8 to c52eefc Compare March 24, 2025 09:11
@rust-log-analyzer

This comment has been minimized.

Kobzol added 2 commits March 24, 2025 11:14
It was always called with `Some`, so no need to complicate it with `Option`.
@bors
Copy link
Contributor

bors commented Mar 24, 2025

☔ The latest upstream changes (presumably #138878) made this pull request unmergeable. Please resolve the merge conflicts.

jhpratt added a commit to jhpratt/rust that referenced this pull request Mar 27, 2025
…ozkan

Remove unneeded LLVM CI test assertions

The `download_ci_llvm` bootstrap test was checking implementation details of the LLVM CI download check, which isn't very useful. It was essentially testing "if function_that_checks_if_llvm_ci_is_available returns true, we enable CI LLVM", but the usage of the function was an implementation detail. After rust-lang#138704, the inner implementation has changed, so the test now breaks if LLVM is updated.

I don't think that it's very useful to test implementation details like this, without taking the outside git state into account. Ideally, we should mock the git state for the test, otherwise the test will randomly break when executed in environments which the test does not control (e.g. on CI when a LLVM change happens).

I only kept the part of the test that checks that LLVM CI isn't used when we specify `download-ci-llvm = false`, as that should hold under all conditions, CI/local, and all git states.

I also kept the `if-unchanged` assertion, but only on CI, and as a temporary measure. After rust-lang#138591, we should have a proper way of mocking the git state to make the test robust, and make it test what we actually want.

Fixes [this](rust-lang#138784 (comment)).

r? `@ghost`
rust-timer added a commit to rust-lang-ci/rust that referenced this pull request Mar 27, 2025
Rollup merge of rust-lang#139015 - Kobzol:llvm-ci-test-fixes, r=onur-ozkan

Remove unneeded LLVM CI test assertions

The `download_ci_llvm` bootstrap test was checking implementation details of the LLVM CI download check, which isn't very useful. It was essentially testing "if function_that_checks_if_llvm_ci_is_available returns true, we enable CI LLVM", but the usage of the function was an implementation detail. After rust-lang#138704, the inner implementation has changed, so the test now breaks if LLVM is updated.

I don't think that it's very useful to test implementation details like this, without taking the outside git state into account. Ideally, we should mock the git state for the test, otherwise the test will randomly break when executed in environments which the test does not control (e.g. on CI when a LLVM change happens).

I only kept the part of the test that checks that LLVM CI isn't used when we specify `download-ci-llvm = false`, as that should hold under all conditions, CI/local, and all git states.

I also kept the `if-unchanged` assertion, but only on CI, and as a temporary measure. After rust-lang#138591, we should have a proper way of mocking the git state to make the test robust, and make it test what we actually want.

Fixes [this](rust-lang#138784 (comment)).

r? `@ghost`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-compiletest Area: The compiletest test runner A-testsuite Area: The testsuite used to check the correctness of rustc S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) T-infra Relevant to the infrastructure team, which will review and decide on the PR/issue. T-release Relevant to the release subteam, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants