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

RFC: Project-based Examples for Cargo Projects #2517

Closed
wants to merge 15 commits into from
177 changes: 177 additions & 0 deletions text/0000-cargo-project-based-examples.md
@@ -0,0 +1,177 @@
- Feature Name: cargo_project_based_examples
- Start Date: 2018-08-01
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)
# Summary
[summary]: #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
[motivation]: #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:

1. Might need more external crates than the project itself;
2. 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 file-based 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. These could be two approaches to write examples for them. In the first approach developers 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. In the second approach developers should add all examples one by one into `[workspace]` members, which requires `cargo run -p` to run it and is somehow complex. 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.

[f3]: https://github.com/japaric/f3/blob/master/Cargo.toml

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'.

[yew]: https://github.com/DenisKolodin/yew/tree/master/examples
[stdweb]: https://github.com/koute/stdweb/tree/master/examples
[wasm-bindgen]: https://github.com/rustwasm/wasm-bindgen/tree/master/examples

# Guide-level explanation
[guide-level-explanation]: #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:

1. import external crates that the library project itself won't use
2. 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:

```toml
[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` or `**/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 <NAME>, 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
[reference-level-explanation]: #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
[drawbacks]: #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
[rationale-and-alternatives]: #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.
Copy link
Member

@killercup killercup Aug 6, 2018

Choose a reason for hiding this comment

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

Sorry, I'll be devil's advocate here for a bit.

Another (valid but boring) alternative: Change nothing but actively tell people not to use cargo run --example in some cases. You can use cargo's workspace feature to use a common target directly (faster compile times), and just run cargo commands in the subdirectory. This is what diesel and quicli do. This also allows you to have multiple binaries in your example projects, as well as using your shell prompt's path printing feature as an indicator for the current example project context.

(I'm pretty sure this was already mentioned somewhere but I feel like it should also be listed in this section.)


It may also be suggested that we could somehow *not* implement this project-based cargo examples, but suggest the developers to build an example by adding all of them as the workspace members, and use the `cargo run -p <NAME>` command to run, test or bench it as it indicates the path of the root project, like what we already have in projects like [diesel] and [quicli]. By this way we share the `target` folder with the root project to improve compile speed as well as type less `cd` commands. However as we should add all our examples one by one into workspace if we do this, once we forget to add one newly-created example and users who does not know `cargo run -p` somehow run it by `cd examples/<NAME>` and `cargo run`, a huge `target` folder in the folder of the example would be created before being noticed.
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't quite correct. Once you have a workspace, if you forget to add a new sub-package to the members list, it will refuse to compile. cargo new will also yell at you so you don't forget.


[diesel]: https://github.com/diesel-rs/diesel/blob/master/Cargo.toml#L1-L26
Copy link
Member

Choose a reason for hiding this comment

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

You should always use Github URLs containing the commit hash, otherwise the content will change and the highlighted lines will point to something totally different. You can easy get that by pressing y on any source page on Github.

[quicli]: https://github.com/killercup/quicli/blob/master/Cargo.toml#L38-L44

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
[prior-art]: #prior-art

None by now.

# Unresolved questions
[unresolved-questions]: #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 provide an approach to test or bench *all examples* at one time in the future?
- Should path-related macro `file!()` refer the path related to the root project path?