Strawman pre-RFC: Custom profiles

In general I’m looking for solutions for the motivations of this RFC, this is a strawman proposal that solves these problems, however other ideas would be good too (and I’ve mentioned some in the RFC). In general I’m not sure what the best way to approach this problem is, there are multiple possible ways of solving it, and it would be good to have some rough consensus on which direction to go in before I write a proper RFC.


Summary

Add the ability to create custom profiles in Cargo.toml, to provide further control over how the project is built. Allow overriding profiles for certain dependency trees.

Motivation

Currently the “stable” way to tweak build parameters like “debug symbols”, “debug assertions”, and “optimization level” is to edit Cargo.toml.

This file is typically checked in tree, so for many projects overriding things involves making temporary changes to this, which feels hacky. On top of this, if Cargo is being called by an encompassing build system as what happens in Firefox, these changes can seem surprising. There are currently two main profiles in Cargo (“dev” and “release”), and we’re forced to fit everything we need into these two categories. This isn’t really enough.

Furthermore, this doesn’t allow for much customization. For example, when trying to optimize for compilation speed by building in debug mode, build scripts will get built in debug mode as well. In case of complex build-time dependencies like bindgen, this can end up significantly slowing down compilation. It would be nice to be able to say “build in debug mode, but build build dependencies in release”. Also, your program may have large dependencies that it doesn’t use in critical paths, being able to ask for just these dependencies to be run in debug mode would be nice.

Guide-level explanation

Currently, the Cargo guide has a section on this.

We amend this to add that you can define custom profiles with the profile.foo key syntax. These can be invoked via cargo build --profile foo. The dev/doc/bench/etc profiles remain special.

Custom profiles can also be “included” into other profiles. This can be done with an include key:

[profile.dev]
debug = true
include = my_custom_profile # implies opt-level = 4, lto = false

[profile.my_custom_profile]
opt-level = 2
lto = false

The include key can scope where it gets included.

[profile.dev]
debug = true
opt-level = 0
[profile.dev.include]
profile = my_custom_profile
type = ["build"] # can be "build", "direct", or both, default is both

[profile.my_custom_profile]
opt-level = 2

This will mean that build scripts and all dependencies of build scripts will be built with my_custom_profile.

You can also scope to specific crates:

[profile.dev]
debug = true
opt-level = 0
[profile.dev.include]
profile = my_custom_profile
crates = ["syn", "regex=0.0.4"] # can specify dependencies or full semver dependency specs

[profile.my_custom_profile]
opt-level = 2

which will mean that only the listed crates will inherit this specification.

Reference-level explanation

(Unsure if there’s much more that can go here)

In case of conflicting requirements imposed by multiple includes, the one mentioned latest in the include tree (preorder) is used.

You can mention multiple includes at once, using the TOML [[key]] syntax:

[profile.dev]
debug = true
opt-level = 0
[[profile.dev.include]]
profile = foo
crates = ["foo"]
[[profile.dev.include]]
profile = bar
crates = ["bar"]

Drawbacks

This complicates cargo.

Rationale and alternatives

There are really two or three concerns here:

  • A stable interface for setting various profile keys (cargo rustc -- -Clto is not good, for example, and doesn’t integrate into Cargo’s target directories)
  • The ability to use a different profile for build scripts (usually, the ability to flip optimization modes; I don’t think folks care as much about -g in build scripts)
  • The ability to use a different profile for specific dependencies

The first one can be resolved partially by stabilizing cargo arguments for overriding these. It doesn’t fix the target directory issue, but that might not be a major concern.

The second one can be fixed with a specific build-scripts = release key for profiles.

The third can’t be as easily fixed, however it’s not clear if that’s a major need.

The nice thing about this proposal is that it is able to handle all three of these concerns. However, separate RFCs for separate features could be introduced as well.

In general there are plans for Cargo to support other build systems by making it more modular (so that you can ask it for a build plan and then execute it yourself). Such build systems would be able to provide the ability to override profiles themselves instead. It’s unclear if the general Rust community needs the ability to override profiles.

Unresolved questions

  • Bikeshedding the naming of the keys
  • The priority order when doing resolution
  • What should be done with dependencies that are both build and regular dependencies
6 Likes

cc @alexcrichton @nrc

Oh, this is great! Here are some thoughts I have about the broader profiles issue.

Workflows

Originally, profiles were envisioned not only as a way to specify rustc flags, but as a general mechanism to specify current “workflow”. That is why there are not only dev and release profiles, but also test, bench and doc, and that’s why internally in Cargo current profile is used to drive decisions about what actions to execute (i.e, there’s code roughly like if profile.name == "test" { execute doc tests}). However, this tight coupling of “workflow” and “flags” bits didn’t pan out well, because invocations like cargo test --release make sense. As a result, currently the choice of profile is rather unpredictable (i.e, cargo test uses both dev and tests profiles, and cargo test --release does not use release profile).

So it would be great to simplify this thing to the following:

  1. Profiles are only about compiler flags.
  2. Each compilation uses a single profile.
  3. Each cargo command has a default profile which it uses.

So, something like this:

command profile
cargo test dev
cargo test --release release
cargo test --profile=foo foo
cargo bench release
cargo bench --profile=dev dev

Profile is a function

We should strive to make this new system maximally general, in a sense that it is possible to tweak compilation options of arbitrary crates. We also should layer DRY niceties like “include” on top, but it’s important to be able to fully write by hand any elaborate profile. (Not that I am not proposing to allow arbitrary rustc flags in profiles, I am talking about “profile selection for crate” mechanism here).

So profile is a function, which maps a crate to a set of options like opt-level, debug, etc. In Cargo terminology, the crate is specified by a pair of a package and a target. So, I think the “elaborated” form of profiles might look like this

[package]
name = "awesome_project"

[test]
name = "test1"

[test]
name = "test2"

[dependencies]
serde = "*"
image = "*"


[profile.dev]
opt-level = 0 # wildcard, which apply to all crates
debug = true
[profile.dev.image] # optimize this dependency even in dev
opt-level = 3
[profile.dev.awesome_project.test2] # optimize this particular test
opt-level = 3

Defaults

It makes sense to tweak default profiles a bit. For example, it makes sense to build build-scripts and proc macros in release mode for all profiles, and it can be argued that it makes sense to build dependencies with -O1/-O2 -g for the dev profile. It might also make sense to add a “dependencies wildcard” convenience option to profiles section ([profile.dev.dependencies] opt-level = 3)

Profiles audit

There at least two flags in profile, rpath and panic = 'unwind' which seem like they are global, and don’t really belong to profiles section.

3 Likes

cc @wycats

[profile.dev]
opt-level = 0 # wildcard, which apply to all crates
debug = true
[profile.dev.image] # optimize this dependency even in dev
opt-level = 3
[profile.dev.awesome_project.test2] # optimize this particular test
opt-level = 3

Small correction; this should be [profile.dev.dependency.image], etc. [profile.dev.image] means [profile.dev] image = {..} and that's going to clash with the opt-level and debug stuff.

I considered something like this before, except that it kinda complicated the situation for "build dependency only". However, thinking about it more we can make this work as [profile.dev.build_profile] ....

For example, it makes sense to build build-scripts and proc macros in release mode for all profiles,

Iiiit depends :smile: . I considered adding this as an optional proposal, but then I realized that many build scripts do a very tiny amount of work and it's hard to know if this is worth it.

and it can be argued that it makes sense to build dependencies with -O1/-O2 -g for the dev profile.

Yeah. In Firefox we switched "debug opt mode" (the default) to opt-level=1 because it generates debuggable code that is not horrendously slow, that is also faster to compile.

Having dependencies built like this by default is an interesting proposition. It will probably be controversial.

If we do propose this it should be possible to reconfigure, perhaps something like [profile.dev.dependency.*]

There at least two flags in profile, rpath and panic = 'unwind' which seem like they are global, and don’t really belong to profiles section.

nah, panic=unwind belongs there. You might want to unwind in debug mode and abort in release mode. I'm not clear on rpath, but it seems like it belongs there too (just that you'd not have much of a reason to change it in a profile)

And the build_profile matches all custom builds, not only the one from the current crate, right?

Hm, that's unfortunate! Nested tables in TOML don't look really great, because you have to repeat the table's header... I think in practice we can work around the clashes based on type of the value (table vs atomic) or by just blacklisting opt-level crate name :slight_smile: That is, I think it's probably ok to add implementation level hacks for obscure edge cases to make common case easier. That said, now I think that it indeed makes sense to move overrides behind a separate key. So, something like this:

name = "awesome_project"

[[test]]
name = "test1"

[[test]]
name = "test2"

[dependencies]
serde = "*"
image = "*"


[profile.dev]
opt-level = 0
debug = true

  [profile.dev.overrides]
  [profile.dev.overrides.image]
  opt-level = 3

  [profile.dev.overrides.awesome_project.test2]
  opt-level = 3

  [profile.dev.overrides.dependencies]
  opt-level = 1

  [profile.dev.overrides.build_scripts]
  opt-level = 3

And this definitely looks rather verbose :frowning: I still think that it makes sense to allow for maximally fine-grained specification here, but I am not sure what would be the nice concrete syntax for this. On the over hand, the concrete syntax should not matter much ideally, because the defaults should be good enough for most applications and libraries?

nah, panic=unwind belongs there.

Seems plausible. But it's still a compilation-wide setting, right? You can't link panic=abort and panic=unwind libraries together? So the Cargo will need to check that profile settings are sound in this respect.

Hm, another thought about the surface language. What if instead of nested tables we use a specially-valued keys, like

[profiles."dev:build-scripts"]
opt-level = 3

yeah. it matches all crates that are the transitive dependency of a build script. In case of crates which are included in both, we'd need to figure out what to do here (likely use the build script override in both cases)

That is, I think it’s probably ok to add implementation level hacks for obscure edge cases to make common case easier.

I think this isn't going to work well in the long run as more keys get added :slight_smile: . It also stops us from adding other functionality to profiles easily. Nesting tables under profile.foo.overrides seems ok. We can have profile.foo.build_override for the special build script case; weird syntax will probably be more confusing than informative

This looks great, thanks for doing the work to write this up! From my perspective of using cargo as part of the Firefox build, I have a few comments, and some other random thoughts.

Overall this looks like it'd be generally useful for Rust projects--you could have some more wiggle room in doing useful things, like defining a "release with debug symbols" profile for when you want to profile your code, or a "release with LTO enabled" profile for builds you intend to ship to users and don't care about compile times, etc. I'm still not 100% confident that we'll be able to map the set of Firefox build options into a small set of profiles. I mentioned this on IRC, but I think a useful exercise would be to sort through the full list of Firefox build options that impact Rust compilation to see if we can map them to a fixed set of profiles.

An alternate take here might be to allow defining profiles in the cargo config file, which we're already generating at configure time for the Firefox build (to do source replacement for our vendored crates). If cargo supported that we could very easily generate a [profile.user] with the exact options from configure.

Custom profiles can also be “included” into other profiles. This can be done with an include key:

This seems useful for minimizing duplication.

type = ["build"] # can be "build", "direct", or both, default is both

Two thoughts here:

  1. build doesn't seem like a very clear name to me, despite it matching the autotools convention. I'd prefer something very explicit like build_script, and maybe we could use target instead of direct, since it will functionally mean "when building code for whatever --target you passed".
  2. Currently by default cargo supports {dev,release,test,bench,doc}, and you can override their defaults by setting keys under [profile.name]. What profile does cargo use for build scripts currently, just the same settings as for the target build? It might be useful to flesh out the build script portion of this a bit more. Could we first simply add [profile.build_script] to allow overriding the defaults for build scripts? If you build with a non-default profile what profile does cargo use for build scripts? I could probably go on some more, but I think this merits some more consideration. (Being able to set build script compiler options will be great, btw!)

You can also scope to specific crates:

This is wonderful, and we have immediate need for it in Gecko. I've wanted this in other situations as well. Before we switched sccache to use ring's SHA-512 implementation (which is assembly) we were using the sha1 crate and non-release builds of sccache were basically unusable because the time spent calculating hashes dominated, and a non-optimized build of sha1 was super slow.

You can mention multiple includes at once, using the TOML [[key]] syntax:

I do not love this syntax, TBH. It feels odd to me that both "apply a profile to build scripts" and "apply a profile to specific crates" are part of include. I like @matklad's proposals, but maybe we could make the key names more flexible, similar to how cargo currently allows cfg syntax like [target.'cfg(...)'.dependencies] etc? What about something like:

[profile.dev.overrides.serde]
opt-level = 3

# The key can be a whitespace-separated list of crate names or semver specifications.
[profile.dev.overrides."encoding=0.2 libc"]
opt-level = 2

# Use "*" to specify overrides for all dependent crates (but not this crate). Will need to specify
# interaction between this and specific named overrides.
[profile.dev.overrides."*"]
debug = false

I’ve updated this with feedback from @matklad and @luser . I’ll post an RFC soon.

1 Like

RFC filed.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.