|
| 1 | +- Feature Name: `path_bases` |
| 2 | +- Start Date: 2023-11-13 |
| 3 | +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) |
| 4 | +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +Introduce shared base directories in Cargo configuration files that in |
| 10 | +turn enable base-relative `path` dependencies. |
| 11 | + |
| 12 | +# Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +While developing locally, users may wish to specify many `path` |
| 16 | +dependencies that all live in the same local directory. If that local |
| 17 | +directory is not a short distance from the `Cargo.toml`, this can get |
| 18 | +unwieldy. They may end up with a `Cargo.toml` that contains |
| 19 | + |
| 20 | +```toml |
| 21 | +foo = { path = "/home/jon/dev/rust/foo" } |
| 22 | +bar = { path = "/home/jon/dev/rust/bar" } |
| 23 | +baz = { path = "/home/jon/dev/rust/ws/baz" } |
| 24 | +``` |
| 25 | + |
| 26 | +This is not only frustrating to type out, but also requires many changes |
| 27 | +should any component of the path change. For example, if `foo`, `bar`, |
| 28 | +and `ws/baz` were to move under a sub-directory of `libs`, all the paths |
| 29 | +would have to be updated. If they are used in more than one local |
| 30 | +project, each project would have to be updated. |
| 31 | + |
| 32 | +As related issue arises in contexts where an external build system may |
| 33 | +make certain dependencies available through vendoring. Such a build |
| 34 | +system might place vendored packages under some complex path under a |
| 35 | +build-root, like |
| 36 | + |
| 37 | +``` |
| 38 | +/home/user/workplace/feature-1/build/first-party-package/first-party-package-1.0/x86_64/dev/build/private/rust-vendored/ |
| 39 | +``` |
| 40 | + |
| 41 | +If a developer wishes to use such an auto-vendored dependency, a |
| 42 | +contract must be established with the build system about exactly where |
| 43 | +vendred dependencies will end up. And since that path may not be near |
| 44 | +the project's `Cargo.toml`, the user's `Cargo.toml` may end up with |
| 45 | +either an absolute path or a long relative path, both of which may not |
| 46 | +work on other hosts, and thus cannot be checked in (or must be |
| 47 | +overwritten in-place by the build system). |
| 48 | + |
| 49 | +The proposed mechanism aims to simplify both of these use-cases by |
| 50 | +introducing named "base" paths in the Cargo configuration |
| 51 | +(`.cargo/config.toml`). Path dependencies can then be given relative to |
| 52 | +those base path names, which can be set either by a local developer in |
| 53 | +their user-wide configuration (`~/.cargo/config.toml`), or by an |
| 54 | +external build system in a project-wide configuration file. |
| 55 | + |
| 56 | +This effectively makes a "group" of path dependencies available at some |
| 57 | +undisclosed location to `Cargo.toml`, which then only has to know the |
| 58 | +layout to path dependencies _within_ that directory, and not the path |
| 59 | +_to_ that directory. |
| 60 | + |
| 61 | +# Guide-level explanation |
| 62 | +[guide-level-explanation]: #guide-level-explanation |
| 63 | + |
| 64 | +If you often use path dependencies that live in a particular location, |
| 65 | +or if you want to avoid putting long paths in your `Cargo.toml`, you can |
| 66 | +define path _base directories_ in your [Cargo |
| 67 | +configuration](https://doc.rust-lang.org/cargo/reference/config.html). |
| 68 | +Your path dependencies can then be specified relative to those |
| 69 | +directories. |
| 70 | + |
| 71 | +For example, say you have a number of projects checked out in |
| 72 | +`/home/user/dev/rust/libraries/`. Rather than use that path in your |
| 73 | +`Cargo.toml` files, you can define it as a "base" path in |
| 74 | +`~/.cargo/config.toml`: |
| 75 | + |
| 76 | +```toml |
| 77 | +[base_path] |
| 78 | +dev = "/home/user/dev/rust/libraries/" |
| 79 | +``` |
| 80 | + |
| 81 | +Now, you can specify a path dependency on a library `foo` in that |
| 82 | +directory in your `Cargo.toml` using |
| 83 | + |
| 84 | +```toml |
| 85 | +[dependencies] |
| 86 | +foo = { path = "foo", base = "dev" } |
| 87 | +``` |
| 88 | + |
| 89 | +Like with other path dependencies, keep in mind that both the base _and_ |
| 90 | +the path must exist on any other host where you want to use the same |
| 91 | +`Cargo.toml` to build your project. |
| 92 | + |
| 93 | +# Reference-level explanation |
| 94 | +[reference-level-explanation]: #reference-level-explanation |
| 95 | + |
| 96 | +## Configuration |
| 97 | + |
| 98 | +`[base_path]` |
| 99 | + |
| 100 | +* Type: string |
| 101 | +* Default: see below |
| 102 | +* Environment: `CARGO_BASE_PATH_<name>` |
| 103 | + |
| 104 | +The `[base_path]` table defines a set of path prefixes that can be used to |
| 105 | +prepend the locations of `path` dependencies. Each key in the table is the name |
| 106 | +of the base path and the value is the actual file system path. These base paths |
| 107 | +can be used in a `path` dependency by setting its `base` key to the name of the |
| 108 | +base path to use. |
| 109 | + |
| 110 | +```toml |
| 111 | +[base_path] |
| 112 | +dev = "/home/user/dev/rust/libraries/" |
| 113 | +``` |
| 114 | + |
| 115 | +The "dev" base path may then be referenced in a `Cargo.toml`: |
| 116 | + |
| 117 | +```toml |
| 118 | +[dependencies] |
| 119 | +foo = { path = "foo", base = "dev" } |
| 120 | +``` |
| 121 | + |
| 122 | +To produce a `path` dependency `foo` located at |
| 123 | +`/home/user/dev/rust/libraries/foo`. |
| 124 | + |
| 125 | + |
| 126 | +## Specifying Dependencies |
| 127 | + |
| 128 | +A `path` dependency may optionally specify a base path by setting the `base` key |
| 129 | +to the name of a base path from the `[base_path]` table in the configuration. |
| 130 | +The value of that base path in the configuration is prepended to the `path` |
| 131 | +value to produce the actual location where Cargo will look for the dependency. |
| 132 | + |
| 133 | +If the base path is not found in the `[base_path]` table then Cargo will |
| 134 | +generate an error. |
| 135 | + |
| 136 | +```toml |
| 137 | +[dependencies] |
| 138 | +foo = { path = "foo", base = "dev" } |
| 139 | +``` |
| 140 | + |
| 141 | +Given a `[base_path]` table in the configuration that contains: |
| 142 | + |
| 143 | +```toml |
| 144 | +[base_path] |
| 145 | +dev = "/home/user/dev/rust/libraries/" |
| 146 | +``` |
| 147 | + |
| 148 | +Will then produce a `path` dependency `foo` located at |
| 149 | +`/home/user/dev/rust/libraries/foo`. |
| 150 | + |
| 151 | +# Drawbacks |
| 152 | +[drawbacks]: #drawbacks |
| 153 | + |
| 154 | +1. There is now an additional way to specify a dependency in |
| 155 | + `Cargo.toml` that may not be accessible when others try to build the |
| 156 | + same project. Specifically, it may now be that the other host has a |
| 157 | + `path` dependency available at the same relative path to `Cargo.toml` |
| 158 | + as the author of the `Cargo.toml` entry, but does not have the `base` |
| 159 | + defined (or has it defined as some other value). |
| 160 | + |
| 161 | + At the same time, this might make path dependencies _more_ re-usable |
| 162 | + across hosts, since developers can dictate only which _bases_ need to |
| 163 | + exist, rather than which _paths_ need to exist. This would allow |
| 164 | + different developers to host their path dependencies in different |
| 165 | + locations from the original author. |
| 166 | +2. Developers still need to know the path _within_ each path base. We |
| 167 | + could instead define path "aliases", though at that point the whole |
| 168 | + thing looks more like a special kind of "local path registry". |
| 169 | +3. This introduces yet another mechanism for grouping local |
| 170 | + dependencies. We already have [local registries, directory |
| 171 | + registries](https://doc.rust-lang.org/cargo/reference/source-replacement.html), |
| 172 | + and the [`[paths]` |
| 173 | + override](https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#paths-overrides). |
| 174 | + However, those are all intended for immutable local copies of |
| 175 | + dependencies where versioning is enforced, rather than as mutable |
| 176 | + path dependencies. |
| 177 | + |
| 178 | +# Rationale and alternatives |
| 179 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 180 | + |
| 181 | +This design was primarily chosen for its simplicity — it adds very |
| 182 | +little to what we have today both in terms of API surface and mechanism. |
| 183 | +But, other approaches exist. |
| 184 | + |
| 185 | +Developers could have their `path` dependencies point to symlinks in the |
| 186 | +current directory, which other developers would then be told to set up |
| 187 | +to point to the appropriate place on their system. This approach has two |
| 188 | +main drawbacks: they are harder to use on Windows as they [require |
| 189 | +special privileges](https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links), |
| 190 | +and they pollute the user's project directory. |
| 191 | + |
| 192 | +For the build-system case, the build system could place vendored |
| 193 | +dependencies directly into the source directory at well-known locations, |
| 194 | +though this would mean that if the source of those dependencies were to |
| 195 | +change, the user would have to re-run the build system (rather than just |
| 196 | +run `cargo`) to refresh the vendored dependency. And this approach too |
| 197 | +would end up polluting the user's source directory. |
| 198 | + |
| 199 | +An earlier iteration of the design avoided adding a new field to |
| 200 | +dependencies, and instead inlined the base name into the path using |
| 201 | +`path = "base::relative/path"`. This has the advantage of not |
| 202 | +introducing another special keyword in `Cargo.toml`, but comes at the |
| 203 | +cost of making `::` illegal in paths, which was deemed too great. |
| 204 | + |
| 205 | +Alternatively, we could add support for extrapolating environment |
| 206 | +variables (or arbitrary configuration values?) in `Cargo.toml` values. |
| 207 | +That way, the path could be given as `path = |
| 208 | +"${base.name}/relative/path"`. While that works, it's not trivially |
| 209 | +backwards compatible, may be confusing when users try to extrapolate |
| 210 | +random other configuration variables in their paths, and _seems_ like a |
| 211 | +possible Pandora's box of corner-cases. |
| 212 | + |
| 213 | +The [`[paths]` |
| 214 | +feature](https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#paths-overrides) |
| 215 | +could be updated to lift its current limitations around adding |
| 216 | +dependencies and requiring that the dependencies be available on |
| 217 | +crates.io. This would allow users to avoid `path` dependencies in more |
| 218 | +cases, but makes the replacement more implicit than explicit. That |
| 219 | +change is also more likely to break existing users, and to involve |
| 220 | +significant refactoring of the existing mechanism. |
| 221 | + |
| 222 | +We could add another type of local registry that is explicitly declared |
| 223 | +in `Cargo.toml`, and from which local dependencies could then be drawn. |
| 224 | +Something like: |
| 225 | + |
| 226 | +```toml |
| 227 | +[registry.local] |
| 228 | +path = "/path/to/path/registry" |
| 229 | +``` |
| 230 | + |
| 231 | +This would make specifying the dependencies somewhat nicer (`version = |
| 232 | +"1", registry = "local"`), and would ensure a standard layout for the |
| 233 | +locations of the local dependencies. However, using local dependencies |
| 234 | +in this manner would require more set-up to arrange for the right |
| 235 | +registry layout, and we would be introducing what is effectively a |
| 236 | +mutable registry, which Cargo has avoided thus far. |
| 237 | + |
| 238 | +Even with such an approach, there are benefits to being able to not put |
| 239 | +complex paths into `Cargo.toml` as they may differ on other build hosts. |
| 240 | +So, a mechanism for indirecting through a path name may still be |
| 241 | +desirable. |
| 242 | + |
| 243 | +Ultimately, by not having a mechanism to name paths that lives outside |
| 244 | +of `Cargo.toml`, we are forcing developers to coordinate their file |
| 245 | +system layouts without giving them a mechanism for doing so. Or to work |
| 246 | +around the lack of a mechanism by requiring developers to add symlinks |
| 247 | +in strategic locations, cluttering their directories. The proposed |
| 248 | +mechanism is simple to understand and to use, and still covers a wide |
| 249 | +variety of use-cases. |
| 250 | + |
| 251 | +# Prior art |
| 252 | +[prior-art]: #prior-art |
| 253 | + |
| 254 | +Python searches for dependencies by walking `sys.path` in definition |
| 255 | +order, which [is pulled |
| 256 | +from](https://docs.python.org/3/tutorial/modules.html#the-module-search-path) |
| 257 | +the current directory, `PYTHONPATH`, and a list of system-wide library |
| 258 | +directories. All imports are thus "relative" to every directory in |
| 259 | +`sys.path`. This makes it easy to inject local development dependencies |
| 260 | +simply by injecting a path early in `sys.path`. The path dependency is |
| 261 | +never made explicit anywhere in Python. We _could_ adopt a similar |
| 262 | +approach by declaring an environment variable `CARGO_PATHS`, where every |
| 263 | +`path` is considered relative to each path in `CARGO_PATHS` until a path |
| 264 | +that exists is found. However, this introduces additional possibilities |
| 265 | +for user confusion if, say, `foo` exists in multiple paths in |
| 266 | +`CARGO_PATHS` and the first one is picked (though maybe that could be a |
| 267 | +warning?). |
| 268 | + |
| 269 | +NodeJS (with npm) is very similar to Python, except that dependencies |
| 270 | +can also be |
| 271 | +[specified](https://nodejs.org/api/modules.html#modules_all_together) |
| 272 | +using relative paths like Cargo's `path` dependencies. For non-path |
| 273 | +dependencies, it searches in [`node_modules/` in every parent |
| 274 | +directory](https://nodejs.org/api/modules.html#modules_loading_from_node_modules_folders), |
| 275 | +as well as in the [`NODE_PATH` search |
| 276 | +path](https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders). |
| 277 | +There does not exist a standard mechanism to specify a path dependency |
| 278 | +relative to a path named elsewhere. With CommonJS modules, JavaScript |
| 279 | +developers are able to extrapolate variables directly into their |
| 280 | +`require` arguments, and can thus implement custom schemes for getting |
| 281 | +customizable paths. |
| 282 | + |
| 283 | +Ruby's `Gemfile` [path |
| 284 | +dependencies](https://bundler.io/man/gemfile.5.html#PATH) are only ever |
| 285 | +absolute paths or paths relative to the `Gemfile`'s location, and so are |
| 286 | +similar to Rust's current `path` dependencies. |
| 287 | + |
| 288 | +The same is the case for Go's `go.mod` [replacement |
| 289 | +dependencies](https://golang.org/doc/modules/managing-dependencies#tmp_10), |
| 290 | +which only allow absolute or relative paths. |
| 291 | + |
| 292 | +From this, it's clear that other major languages do not have a feature |
| 293 | +quite like this. This is likely because path dependencies are assumed |
| 294 | +to be short-lived and local, and thus having them be host-specific is |
| 295 | +often good enough. However, as the motivation section of this RFC |
| 296 | +outlines, there are still use-cases where a simple name-indirection |
| 297 | +could help. |
| 298 | + |
| 299 | +# Unresolved questions |
| 300 | +[unresolved-questions]: #unresolved-questions |
| 301 | + |
| 302 | +- What should the Cargo configuration table and dependency key be called? This |
| 303 | + RFC calls the configuration table `base_path` to be explicit that it is |
| 304 | + dealing with paths (as `base` would be ambiguous) but calls the key `base` to |
| 305 | + keep it concise. |
| 306 | +- Is there other reasonable behavior we could fall back to if a `base` |
| 307 | + is specified for a dependency, but no base by that name exists in the |
| 308 | + current Cargo configuration? This RFC suggests that this should be an |
| 309 | + error, but perhaps there is a reasonable thing to try _first_ prior to |
| 310 | + yielding an error. |
| 311 | + |
| 312 | +# Future possibilities |
| 313 | +[future-possibilities]: #future-possibilities |
| 314 | + |
| 315 | +It seems reasonable to extend `base` to `git` dependencies, with |
| 316 | +something like: |
| 317 | + |
| 318 | +```toml |
| 319 | +[base_path] |
| 320 | +gh = "https://github.com/jonhoo" |
| 321 | +``` |
| 322 | + |
| 323 | +```toml |
| 324 | +[dependency] |
| 325 | +foo = { git = "foo.git", base = "gh" } |
| 326 | +``` |
| 327 | + |
| 328 | +However, this may get complicated if someone specifies `git`, `path`, |
| 329 | +_and_ `base`. |
| 330 | + |
| 331 | +It may also be useful to be able to use `base` for `patch` and `path`. |
0 commit comments