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

cargo add should "Do the Right Thing" for workspaces #480

Closed
epage opened this issue Aug 25, 2021 · 6 comments · Fixed by #504
Closed

cargo add should "Do the Right Thing" for workspaces #480

epage opened this issue Aug 25, 2021 · 6 comments · Fixed by #504

Comments

@epage
Copy link
Collaborator

epage commented Aug 25, 2021

This is motivated by a discussion at crate-ci/cargo-release#288

Currently, cargo add will

  • dep by path: use the path
  • dep by name: look up version in crates.io and uses it

For workspaces, users generally want

  • always use path dependency
  • If crate being modified is publishable and the dependency is non-dev, use the version that is currently in the workspace

I propose we change cargo add to:

  • If user specifies name@ver, name-as-uri, --git, or --ver, --path, process as we do today
  • If name is in workspace (either as path or name),
    • always use path dependency
    • If crate being modified is publishable and the dependency is non-dev, use the version that is currently in the workspace

For the rare times this isn't the desired behavior, it would be relatively trivial for users to adjust. However, knowing the best practice for adding a workspace dep for new users is tricky

  • With cargo-release, we've seen people use [patch] because it was the first way they discovered doing this
  • It requires hand copying the version over when specifying path
  • The version is flat out wrong when just specifying a name
  • Its not obvious that dev-dependency doesn't need version which is important when there are test cycles
@epage
Copy link
Collaborator Author

epage commented Sep 4, 2021

@ordian does this sound good for moving forward? I'd like to have good workspace workflow out in people's hands being vetted before rust-lang/cargo#5586 so we don't have fret over how much we can change things later once this is in cargo.

@ordian
Copy link
Collaborator

ordian commented Sep 5, 2021

There is already -p flag that is consistent with what cargo does in the workspace, e.g. cargo build -p <subcrate> <FLAGS>.
Are you suggesting cargo add <subcrate> <dep> syntax instead? I don't find that a good idea TBH, but maybe I misunderstood you.
Sorry, looks like I misunderstood. You suggest cargo add <dep>, where <dep> is a crate in a workspace? Yeah, automatically inferring that it exists in the workspace and suggesting to use path might be a good idea. But I would give users a choice with ([Y/n]) option.

Wrt using the currently used version, what if there are multiple used in a workspace? Not sure if I understood correctly, but I don't think that deduplicating non-semver compatible versions is in scope of cargo add.

@epage
Copy link
Collaborator Author

epage commented Sep 6, 2021

My proposal is that when a user does cargo add <dep> that if <dep> is a member of the workspace and no other information is provided, we automatically do a path + workspace version.

Can a you have multiple versions of a crate be workspace members at the same time? If so, I'd suspect that is a corner case and would either pick the latest or error and tell the user to choose one.

I just found out about https://github.com/rust-lang/rfcs/blob/master/text/2906-cargo-workspace-deduplicate.md which will be a big help in these situations. I want to finish with cargo-edit first but I want to tackle that soon after.

@epage
Copy link
Collaborator Author

epage commented Sep 6, 2021

Also, I think a prompt would be unnecessary. This is standard enough behavior that prompting people would just be frustrating rather than helpful. Generally, people also have version control to both tell them what happened and help them back out a change.

Too many times, tools mix critical with non-critical prompts and I start auto-"y"-ing through and say yes to the wrong ones.

@ordian
Copy link
Collaborator

ordian commented Sep 7, 2021

Yeah, I don't have statistics on what users actually want when they add workspace deps, that's why suggested the prompt idea but it might be not that good if there only one right answer to that (or in 99% cases). Anyway, I don't have a strong opinion here and PRs are welcome :)

@vjpr
Copy link

vjpr commented Sep 7, 2021

In JS-land, pnpm/yarn add pkg will add pkg as a workspace dependency with path as @epage has suggested. Works well.

https://pnpm.io/cli/add might be an interesting read to see some of the various workspace use-cases that might be good to support.

epage added a commit to epage/cargo-edit that referenced this issue Sep 21, 2021
With killercup#480, we'll be parsing the `Cargo.toml`s which will fail with our
test "versions".  To make these still stand out, I used the metadata field and
chose a ridiculously large version.
epage added a commit to epage/cargo-edit that referenced this issue Sep 21, 2021
If the user doesn't specify anything beyond a positional name or path,
we'll check if its a workspace member and add it according to workspace
best practices
- Use `version` and `path` for published dependencies so it uses the
  local version for dev and the upcoming version on publish
- Use `path` for unpublished dependencies for development without
  creating version bumping cycles.

Fixes killercup#480
epage added a commit to epage/cargo-edit that referenced this issue Sep 21, 2021
If the user doesn't specify anything beyond a positional name or path,
we'll check if its a workspace member and add it according to workspace
best practices
- Use `version` and `path` for published dependencies so it uses the
  local version for dev and the upcoming version on publish
- Use `path` for unpublished dependencies for development without
  creating version bumping cycles.

Fixes killercup#480
ordian pushed a commit that referenced this issue Sep 22, 2021
If the user doesn't specify anything beyond a positional name or path,
we'll check if its a workspace member and add it according to workspace
best practices
- Use `version` and `path` for published dependencies so it uses the
  local version for dev and the upcoming version on publish
- Use `path` for unpublished dependencies for development without
  creating version bumping cycles.

Fixes #480
bors added a commit to rust-lang/cargo that referenced this issue Apr 18, 2022
feat: Import cargo-add into cargo

### Motivation

The reasons I'm aware of are:
- Large interest, see #5586
- Make it easier to add a dependency when you don't care about the version (instead of having to find it or just using the major version if thats all you remember)
- Provide a guided experience, including
  - Catch or prevent errors earlier in the process
  - Bring the Manifest format documentation into the terminal via `cargo add --help`
  - Using `version` and `path` for `dependencies` but `path` only for `dev-dependencies` (see crate-ci/cargo-release#288 which led to killercup/cargo-edit#480)

### Drawbacks

1. This is another area of consideration for new RFCs, like rust-lang/rfcs#3143 (this PR supports it) or rust-lang/rfcs#2906 (implementing it will require updating `cargo-add`)

2. This is a high UX feature that will draw a lot of attention (ie Issue influx)

e.g.
- killercup/cargo-edit#521
- killercup/cargo-edit#126
- killercup/cargo-edit#217

We've tried to reduce the UX influx by focusing the scope to preserving semantic information (custom sort order, comments, etc) but being opinionated on syntax (style of strings, etc)

### Behavior

Help output
<details>

```console
$ cargo run -- add --help
cargo-add                                                                                                                                  [4/4594]
Add dependencies to a Cargo.toml manifest file

USAGE:
    cargo add [OPTIONS] <DEP>[`@<VERSION>]` ...
    cargo add [OPTIONS] --path <PATH> ...
    cargo add [OPTIONS] --git <URL> ...

ARGS:
    <DEP_ID>...    Reference to a package to add as a dependency

OPTIONS:
        --no-default-features     Disable the default features
        --default-features        Re-enable the default features
    -F, --features <FEATURES>     Space-separated list of features to add
        --optional                Mark the dependency as optional
    -v, --verbose                 Use verbose output (-vv very verbose/build.rs output)
        --no-optional             Mark the dependency as required
        --color <WHEN>            Coloring: auto, always, never
        --rename <NAME>           Rename the dependency
        --frozen                  Require Cargo.lock and cache are up to date
        --manifest-path <PATH>    Path to Cargo.toml
        --locked                  Require Cargo.lock is up to date
    -p, --package <SPEC>          Package to modify
        --offline                 Run without accessing the network
        --config <KEY=VALUE>      Override a configuration value (unstable)
    -q, --quiet                   Do not print cargo log messages
        --dry-run                 Don't actually write the manifest
    -Z <FLAG>                     Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for
                                  details
    -h, --help                    Print help information

SOURCE:
        --path <PATH>        Filesystem path to local crate to add
        --git <URI>          Git repository location
        --branch <BRANCH>    Git branch to download the crate from
        --tag <TAG>          Git tag to download the crate from
        --rev <REV>          Git reference to download the crate from
        --registry <NAME>    Package registry for this dependency

SECTION:
        --dev                Add as development dependency
        --build              Add as build dependency
        --target <TARGET>    Add as dependency to the given target platform

EXAMPLES:
  $ cargo add regex --build
  $ cargo add trycmd --dev
  $ cargo add --path ./crate/parser/
  $ cargo add serde serde_json -F serde/derive
```

</details>

Example commands
```rust
cargo add regex
cargo add regex serde
cargo add regex@1
cargo add regex@~1.0
cargo add --path ../dependency
```
For an exhaustive set of examples, see [tests](https://github.com/killercup/cargo-edit/blob/merge-add/crates/cargo-add/tests/testsuite/cargo_add.rs) and associated snapshots

Particular points
- Effectively there are two modes
  - Fill in any relevant field for one package
  - Add multiple packages, erroring for fields that are package-specific (`--rename`)
  - Note that `--git` and `--path` only accept multiple packages from that one source
- We infer if the `dependencies` table is sorted and preserve that sorting when adding a new dependency
- Adding a workspace dependency
  - dev-dependencies always use path
  - all other dependencies use version + path
- Behavior is idempotent, allowing you to run `cargo add serde serde_json -F serde/derive` safely if you already had a dependency on `serde` but without `serde_json`
- When a registry dependency's version req is unspecified, we'll first reuse the version req from another dependency section in the manifest.  If that doesn't exist, we'll use the latest version in the registry as the version req

### Additional decisions

Accepting the proposed `cargo-add` as-is assumes the acceptance of the following:
- Add the `-F` short-hand for `--features` to all relevant cargo commands
- Support ``@`` in pkgids in other commands where we accept `:`
- Add support for `<name>`@<version>`` in more commands, like `cargo yank` and `cargo install`

### Alternatives

- Use `:` instead of ``@`` for versions
- Flags like `--features`, `--optional`, `--no-default-features` would be position-sensitive, ie they would only apply to the crate immediate preceding them
  - This removes the dual-mode nature of the command and remove the need for the `+feature` syntax (`cargo add serde -F derive serde_json`)
  - There was concern over the rarity of position-sensitive flags in CLIs for adopting it here
- Support a `--sort` flag to sort the dependencies (existed previously)
  - To keep the scope small, we didn't want general manifest editing capabilities
- `--upgrade <POLICY>` flag to choose constraint (existed previously)
  - The flag was confusing as-is and we feel we should instead encourage people towards `^`
- `--allow-prerelease` so a `cargo add clap` can choose among pre-releases as well
  - We felt the pre-release story is too weak in cargo-generally atm for making it first class in `cargo-add`
- Offer `cargo add serde +derive serde_json` as a shorthand
- Infer path from a positional argument

### Prior Art

- *(Python)* [poetry add](https://python-poetry.org/docs/cli/#add)
  - `git+` is needed for inferring git dependencies, no separate  `--git` flags
  - git branch is specified via a URL fragment, instead of a `--branch`
- *(Javascript)* [yarn add](https://yarnpkg.com/cli/add)
  - `name@data` where data can be version, git (with fragment for branch), etc
  - `-E` / `--exact`, `-T` / `--tilde`, `-C` / `--caret` to control version requirement operator instead of `--upgrade <policy>` (also controlled through `defaultSemverRangePrefix` in config)
  - `--cached` for using the lock file (killercup/cargo-edit#41)
  - In addition to `--dev`, it has `--prefer-dev` which will only add the dependency if it doesn't already exist in `dependencies` as well as `dev-dependencies`
  - `--mode update-lockfile` will ensure the lock file gets updated as well
- *(Javascript)* [pnpm-add](https://pnpm.io/cli/add)
- *(Javascript)* npm doesn't have a native solution
  - Specify version with ``@<version>``
  - Also overloads `<name>[`@<version>]`` with path and repo
    - Supports a git host-specific protocol for shorthand, like `github:user/repo`
    - Uses fragment for git ref, seems to have some kind of special semver syntax for tags?
  - Only supports `--save-exact` / `-E` for operators outside of the default
- *(Go)* [go get](https://go.dev/ref/mod#go-get)
  - Specify version with ``@<version>``
  - Remove dependency with ``@none``
- *(Haskell)* stack doesn't seem to have a native solution
- *(Julia)* [pkg Add](https://docs.julialang.org/en/v1/stdlib/Pkg/)
- *(Ruby)* [bundle add](https://bundler.io/v2.2/man/bundle-add.1.html)
  - Uses `--version` / `-v` instead of `--vers` (we use `--vers` because of `--version` / `-V`)
  - `--source` instead of `path` (`path` correlates to manifest field)
  - Uses `--git` / `--branch` like `cargo-add`
- *(Dart)* [pub add](https://dart.dev/tools/pub/cmd/pub-add)
  - Uses `--git-url` instead of `--git`
  - Uses `--git-ref` instead of `--branch`, `--tag`, `--rev`

### Future Possibilities

- Update lock file accordingly
- Exploring the idea of a [`--local` flag](killercup/cargo-edit#590)
- Take the MSRV into account when automatically creating version req (killercup/cargo-edit#587)
- Integrate rustsec to report advisories on new dependencies (killercup/cargo-edit#512)
- Integrate with licensing to report license, block add, etc (e.g. killercup/cargo-edit#386)
- Pull version from lock file (killercup/cargo-edit#41)
- Exploring if any vendoring integration would be beneficial (currently errors)
- Upstream `cargo-rm` (#10520), `cargo-upgrade` (#10498), and `cargo-set-version` (in that order of priority)
- Update crates.io with `cargo add` snippets in addition to or replacing the manifest snippets

For more, see https://github.com/killercup/cargo-edit/issues?q=is%3Aissue+is%3Aopen+label%3Acargo-add

### How should we test and review this PR?

This is intentionally broken up into several commits to help reviewing
1. Import of production code from cargo-edit's `merge-add` branch, with only changes made to let it compile (e.g. fixing up of `use` statements).
2. Import of test code / snapshots.  The only changes outside of the import were to add the `snapbox` dev-dependency and to `mod cargo_add` into the testsuite
3. This extends the work in #10425 so I could add back in the color highlighting I had to remove as part of switching `cargo-add` from direct termcolor calls to calling into `Shell`

Structure-wise, this is similar to other commands
- `bin` only defines a CLI and adapts it to an `AddOptions`
- `ops` contains a focused API with everything buried under it

The "op" contains a directory, instead of just a file, because of the amount of content.  Currently, all editing code is contained in there.  Most of this will be broken out and reused when other `cargo-edit` commands are added but holding off on that for now to separate out the editing API discussions from just getting the command in.

Within the github UI, I'd recommend looking at individual commits (and the `merge-add` branch if interested), skipping commit 2.  Commit 2 would be easier to browse locally.

`cargo-add` is mostly covered by end-to-end tests written using `snapbox`, including error cases.

There is additional cleanup that would ideally happen that was excluded intentionally from this PR to keep it better scoped, including
- Consolidating environment variables for end-to-end tests of `cargo`
- Pulling out the editing API, as previously mentioned
  - Where the editing API should live (`cargo::edit`?)
  - Any more specific naming of types to reduce clashes (e.g. `Dependency` or `Manifest` being fairly generic).
- Possibly sharing `SourceId` creation between `cargo install` and `cargo edit`
- Explore using `snapbox` in more of cargo's tests

Implementation justifications:
- `dunce` and `pathdiff` dependencies: needed for taking paths relative to the user and make them relative to the manifest being edited
- `indexmap` dependency (already a transitive dependency): Useful for preserving uniqueness while preserving order, like with feature values
- `snapbox` dev-dependency: Originally it was used to make it easy to update tests as the UX changed in prep for merging but it had the added benefit of making some UX bugs easier to notice so they got fixed.  Overall, I'd like to see it become the cargo-agnostic version of `cargo-test-support` so there is a larger impact when improvements are made
- `parse_feature` function: `CliFeatures` forces items through a `BTreeSet`, losing the users specified order which we wanted to preserve.

### Additional Information

See also [the internals thread](https://internals.rust-lang.org/t/feedback-on-cargo-add-before-its-merged/16024).

Fixes #5586
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants