Using open source software can be a double-edged sword: We enjoy the latest features and innovations, but we hate frequent and sometimes tedious upgrades.
Bevy is a fast and flexible game engine written in Rust. It aims to provide a modern and modular architecture, notably Entity Component System(ECS), that allows developers to craft rich and interactive experiences.
However, the shiny new engine is also an evolving project that periodically introduces breaking changes in its API.
Bevy’s migration guide is comprehensive, but daunting. It is sometimes overwhelmingly long because it covers many topics and scenarios.
In this article, we will show you how to make migration easier by using some command line tools such as
ast-grep. These tools can help you track the changes, search for specific patterns in your code, and automate API migration. Hope you can migrate your Bevy projects with less hassle and more confidence by following our tips.
We will use the utility AI library big-brain, the second most starred Bevy project on GitHub, as an example to illustrate bumping Bevy version from 0.9 to 0.10.
Upgrading consists of four big steps: make a clean git branch, updating the dependencies, running fix commands, and fixing failing tests. And here is a list of commands used in the migration.
git: Manage code history, keep code snapshot, and help you revert changes if needed.
cargo check: Quickly check code for errors and warnings without building it.
ast-grep: Search for ASTs in source and automate code rewrite using patterns or expressions.
cargo fmt: Format the rewritten code according to Rust style guidelines.
cargo test: Run tests in the project and report the results to ensure the program still works.
install the binary `sg`/`ast-grep`
The first step is to clone your repository to your local machine. You can use the following command to clone the big-brain project:
git clone firstname.lastname@example.org:HerringtonDarkholme/big-brain.git
Note that the big-brain project is not the official repository of the game, but a fork that has not updated its dependencies yet. We use this fork for illustration purposes only.
Next, you need to create a new branch for the migration. This will allow you to keep track of your changes and revert them if something goes wrong. You can use the following command to create and switch to a new branch called
git checkout -b upgrade-bevy
Key take away: make sure you have a clean git history and create a new branch for upgrading.
Now it’s time for us to kick off the real migration! First big step is to update dependencies. It can be a little bit tricker than you think because of transitive dependencies.
Let’s change the dependency file
Cargo.toml. Luckily big-brain has clean dependencies.
Here is the diff:
diff --git a/Cargo.toml b/Cargo.toml
After you have updated your dependencies, you need to build a new lock-file that reflects the changes. You can do this by running the following command:
This will check your code for errors and generate a new Cargo.lock file that contains the exact versions of your dependencies.
You should inspect your Cargo.lock file to make sure that all your dependencies are compatible and use the same version of Bevy. Bevy is more a bazaar than a cathedral. You may install third-party plugins and extensions from the ecosystem besides the core library. This means that some of these crates may not be updated or compatible with the latest version of Bevy or may have different dependencies themselves, causing errors or unexpected behavior in your code.
If you find any inconsistencies, you can go back to step 3 and modify your dependencies accordingly. Repeat this process until your Cargo.lock file is clean and consistent.
A tip here is to search
bevy 0.9 in the lock file.
Cargo.lock will list library with different version numbers.
Fortunately, Bevy is the only dependency in big-brain. So we are good to go now!
Key take away: take advantage of Cargo.lock to find transitive dependencies that need updating.
We will use compiler to spot breaking changes and use AST rewrite tool to repeatedly fix these issues.
This is a semi-automated process because we need to manually check the results and fix the remaining errors.
The mantra here is to use automation that maximize your productivity. Write codemod that is straightforward to you and fix remaining issues by hand.
The first error is quite easy. The compiler outputs the following error.
error[E0432]: unresolved import `CoreStage`
From migration guide:
CoreStage(… more omitted) enums have been replaced with
CoreSet(… more omitted). The same scheduling guarantees have been preserved.
So we just need to change the import name. Using ast-grep is trivial here.
We need to provide a pattern,
-p, for it to search as well as a rewrite string,
-r to replace the old API with the new one. The command should be quite self-explanatory.
sg -p 'CoreStage' -r CoreSet -i
We suggest to add
-i flag for
--interactive editing. ast-grep will display the changed code diff and ask your decision to accept or not.
Our next error is also easy-peasy.
error: cannot find derive macro `StageLabel` in this scope
System labels have been renamed to systems sets and unified with stage labels. The
StageLabeltrait should be replaced by a system set, using the
SystemSettrait as dicussed immediately below.
sg -p 'StageLabel' -r SystemSet -i
The next error is much harder. First, the error complains two breaking changes.
error[E0599]: no method named `add_stage_after` found for mutable reference `&mut bevy::prelude::App` in the current scope
Let’s see what migration guide said. This time we will give the code example.
add_stage_after is removed and
SystemStage is renamed. We should use
Let’s write a command for this code migration.
This pattern deserves some explanation.
$OWN_STAGE are meta-variables.
meta-variable is a wildcard expression that can match any single AST node. So we effectively find all
add_stage_after call. We can also use meta-variables in the rewrite string and ast-grep will replace them with the captured AST nodes. ast-grep’s meta-variables are very similar to regular expression’s dot
., except they are not textual.
However, I found some
add_stage_afters are not replaced. Nah, ast-grep is quite dumb that it cannot handle the optional comma after the last argument. So I used another query with a trailing comma.
Cool! Now it replaced all
Our next error is about
add_system_to_stage. The migration guide told us:
Let’s also write a pattern for it.
The next error corresponds to the system_sets in migration guide.
We need to change
Alas, I don’t know how to write a pattern to fix that. Maybe ast-grep is not strong enough to support this. I just change
It is still faster than me scratching my head about how to automate everything.
Another change is to use
add_systems instead of
add_system_set. This is a simple pattern!
This should fix
- Last error
Our last error is about
error[E0277]: the trait bound `BigBrainStage: BaseSystemSet` is not satisfied
BigBrainStage::Thinkers is not a base set in Bevy, so we should change it to
Hoooray! Finally the program compiles!
ship it Now let’s test it.
Key take away: Automation saves your time! But you don’t have to automate everything.
Congrats! You have automated code refactoring! But ast-grep’s rewrite can be messy and hard to read. Most code-rewriting tool does not support pretty-print, sadly.
A simple solution is to run
cargo fmt and make the repository neat and tidy.
A good practice is to run this command every time after a code rewrite.
Key take away: Format code rewrite as much as you want.
Let’s use Rust’s standard test command to verify our changes:
Oops. we have one test error, not too bad!
running 1 test
Okay, it complains that
Cleanup have a conflicting running order. This is probably caused by
I should have caught the bug during diff review but I missed that. It is not too late to change it manually.
cargo test again?
We failed doc-test!
Because our ast based tool does not process comments. Lame. :(
We need manually fix them.
Finally we passed all tests!
test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.68s
Now we can commit and push our version upgrade to the upstream. It is not a too long battle, is it?
I have created a pull request for reference. https://github.com/HerringtonDarkholme/big-brain/pull/1/files
Reading a long migration guide is not easy, and fixing compiler errors is even harder.
It would be nice if the official guide can contain some automated command to ease the burden. For example, yew.rs did a great job by providing automation in every release note!
To recap our semi-automated refactoring, this is our four steps:
- Keep a clean git branch for upgrading
- Update all dependencies in the project and check lock files.
- Compile, Rewrite, Verify and Format. Repeat this process until the project compiles.
- Run Test and fix the remaining bugs.
I hope this workflow will help you and other programming language developers in the future!