Skip to content

Commit 9654927

Browse files
committed
Add named path bases to cargo (v2)
Introduce shared base directories in Cargo configuration files that in turn enable base-relative path dependencies.
1 parent 48d7d6a commit 9654927

File tree

1 file changed

+331
-0
lines changed

1 file changed

+331
-0
lines changed

text/0000-cargo-path-bases.md

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
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

Comments
 (0)