Summary
This RFC enables cargo to run, test and bench examples which are arranged and stored as a project-based cargo project in examples folder, in addition to existing single examples/*.rs and examples/**/main.rs file-based examples. A project-based example imply a complete cargo project with Cargo.toml, src folder, main.rs and maybe other necessary files in it.
Motivation
Recently many projects, especially huge library projects, are used in a bunch of conditions, and to prove the universal use of the project, a wide variety of examples is needed. But differences are there between examples and the project itself because an example:
- Might need more external crates than the project itself;
- Could be compiled into various cargo targets.
For example, when developing a backend for code editors like Xray, we might need to implement its frontend in a diversity of forms like terminal for vim-like experience, Qt for graphic UI, or even wasm for online judge websites. These forms need to import different crates and are built to different targets. If we only use existing single file examples, we were not even able to import external crates without editing the Cargo.toml for the project itself, which might lead to compiling unnecessary libraries to build this project.
Likewise, other projects, especially developed for embedded platforms, also needs separate dependency for writing examples. When writing examples for them, developers now have to rely on the dev-dependencies and write dependencies together for all examples into the root Cargo.toml, like what projects like f3 did, and we have to compile all of them even if trying to build only one example. If project-based examples could be formed for these projects, developers would be able to run the examples in a more graceful way as well as save compile time.
In addition, many library projects like yew, stdweb and wasm-bindgen, now already somehow made an attempt to place folders into examples folder to form an array of example ‘projects’.
Guide-level explanation
Generalized abstract
Before we start, we should keep in mind that firstly this RFC enhances the search range for parameter --example in subcommand run, test and bench, and secondly this RFC adds --example for subcommand new.
This means, for example, you can create a example project abc using cargo new --example abc, and you may run it with cargo run --example abc as it now searches for all examples/abc.rs file, examples/abc/main.rs and examples/abc/ project folder.
An example project could include its own tests and benches, but to make things more clear, it’s not suggested to have nested example projects inside. Building a nested example project is possible, but as the only way to run, test or bench the nested example is by adding [workspace] members and run with cargo run -p, it’s not convenient by now to deal with the nested example project directly using cargo command from the root project.
When to use project-based examples
When you are developing a library project, trying to build examples in single examples/*.rs files or examples/**/main.rs that could be complex enough to:
- import external crates that the library project itself won’t use
- be compiled into entirely different framework than the library project itself
you might need to change an approach because you are now able to build project-based examples.
Create a project-based example
Assume that you have a library project my_project and you want to build examples for it. So here we go.
Firstly, use cd command to direct your terminal to project root. Now the terminal might shows like this before your cursor:
XXX: my_project username$
Secondly, Create a example project using:
cargo new --example abc
where abc is the name of your example. This command creates a folder for your project-based example abc, which is a complete templated cargo project, in examples folder. So your my_project have a structure like:
.
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── examples
└── abc # created by the command
├── Cargo.lock
├── Cargo.toml # import your crates here
└── src
└── main.rs # write your example code here
By creating an example project, you are now ready to write your code in main.rs and Cargo.toml. Feel free to code here with your editor.
Note that a .gitignore file is added to examples/abc/.gitignore as gitignore is supported in nested folders. A simple line !Cargo.lock will be written in it so that the gitignore rule for Cargo.lock is not to be overridden by the .gitignore file in the root. As the .gitignore in subdirectories effects the directory it is in, the .gitignore in root project is not to be effected.
As for Cargo.toml, for your convenience, cargo automatically generate a dependency on your root project like:
[dependencies]
my_project = { path = "../.." }
Thirdly, after your code work, you need to run your example. Instead of using cd examples/abc and cargo run, which would also works but somehow complicated, you could run your example as simple as:
cargo run --example abc
Then you can happily witness you example code being run in console, just as what you did before to *.rs example files.
Additonally, you may include tests and benches into your example project. You can create tests or benches in examples/abc/ folder, and run it by cargo test --example abc etc. Note that it’s not suggested to create examples for examples.
When conflicting with *.rs ot **/main.rs files
Your examples folder may include more than one of *.rs files, **/main.rs files and/or project-based examples as folders, thus might have conflicting names when for example, you have at least two of examples/abc.rs, examples/abc/main.rs and project examples/abc/.
Yet cargo already have supported *.rs and **/main.rs. If there are conflicts between them, or another word you have both of them in your examples folder, you will get this message when trying to run this example:
error: failed to parse manifest at `<Project Root>/Cargo.toml`
Caused by:
found duplicate example name a, but all example targets must have a unique name
After the third approach implemented as examples/abc/ project, if there are still conflicts, the above message is given as well. And what you need to do to resolve this conflict is to rename the folders or the files to make the names unique.
Notes for path-related macros
As project-based examples might affect path-related macros, there are two macros we should pay attention to: module_path!() and file!(). As project-based examples should be treated like a unique project, the outputs of these two macros should refer to the path of the example instead of root project path. For example, if we have a abc example project for root project my_project and the main function in examples/abc/src/main.rs contains println!("{} {}", module_path!(), file!()), it should print abc src/main.rs rather than abc examples/abc/src/main.rs.
There are plenty of macros like line!() and column!() whose value after being parsed are related to the line number and column number. Fortunately, the parsing logic of these macros above are not effected if being written into project-based example codes.
Conclusion
By including cargo new --example and enhancing cargo run --example etc., this RFC provides a more convenient way for you to write examples for your project.
Reference-level explanation
Enhanced --example for run, test and bench
To implement this feature, firstly we enlarges --example search scope. We’ve already got examples/*.rs and examples/**/main.rs searched by cargo every time it tries to run, test or bench an example. But to implement examples/**/ project we should detect if this folder contains a valid cargo project. This could be done by detecting Cargo.toml the same way we detect when trying to run cargo run in an invalid folder for cargo projects.
When operating example project, we compile the whole example project as what we do on the root project. We could share the target folder with it then incremental compiling can be enabled to save time. The target of example projects are stored just like what we did for file-based examples.
Running, testing and benching project-based examples should be treated the same as what Rust already do for single-file examples. Same toolchain should be applied to them by default.
Operating example projects with --example <NAME> is just like a syntax sugar in programming, which help us save time when executing examples.
For path-related macros, refer to the paragraph ‘Notes for path-related macros’.
cargo new --example <NAME>
Another feature cargo new --example <NAME> requires creating a new argument. If argument --example <NAME> is found, we search for if this folder exists, if so we fire an error like error: example NAME already exists; and if not, a folder is created in examples and a cargo project for runnables where there is main.rs is built inside.
Note that cargo new --example <NAME> can only be executed inside a cargo project. Execution with no cargo project detected should be denied with an error message like error: no cargo project found to create an example or similiar messages.
Additionally, what cargo new --example <NAME> differs from cargo new is only that the former command, as is mentioned above, generates a dependency on the root project using ../.. references. Despite this the procedure should be the same, including what we should write into the .gitignore file for it, so it should be possible to cd into it and run cargo run directly.
Drawbacks
If we implement project-based examples into cargo, it might be backward incompatible if we want to use --example <NAME> in other ways in the future.
Rationale and alternatives
There could be another way to implement project-based examples by introducing cargo example subcommand. However by doing this we must change our way to run examples now by cargo run --example <NAME> which is already widely accepted by rust community.
On .gitignore, it could be a good idea to rewrite the .gitignore file in the root changing the Cargo.lock to /Cargo.lock to avoid it search for every cargo locks nestedly thus a !Cargo.lock is not needed in the example project path. However it would totally change the way how we write .gitignore for Rust, thus this alternative is remained for the Rust authors to judge.
Prior art
None by now.
Unresolved questions
- Is there any more graceful way to replace the
my_project = { path = "../.." } in Cargo.toml file for all example projects?
- Will it be useful if we provided an approach to test or bench all examples at one time?
- Should path-related macro
file!() refer the path related to the root project path?