Skip to content

Build workspace member graph to determine if members are dependencies#14683

Closed
jtfmumm wants to merge 16 commits intorelease/080from
jtfm/workspace-graph
Closed

Build workspace member graph to determine if members are dependencies#14683
jtfmumm wants to merge 16 commits intorelease/080from
jtfm/workspace-graph

Conversation

@jtfmumm
Copy link
Contributor

@jtfmumm jtfmumm commented Jul 17, 2025

This draft PR contains a first pass at building a graph of workspace member dependencies to differentiate members that are graph sources from members that are dependencies. This is for checking whether we should build a member by default (see #14663).

samypr100 and others added 16 commits July 16, 2025 14:26
Closes #13057

Sets `UV_TOOL_BIN_DIR` to `/usr/local/bin` for all derived images to
allow `uv tool install` to work out of the box.

Note, when the default image user is overwritten (e.g. `USER <UID>`) by
a less privileged one, an alternative writable location would now need
to be set by downstream consumers to prevent issues, hence I'm labeling
this as a breaking change for 0.8.x release.

Relates to astral-sh/uv-docker-example#55

Each image was tested to work with uv tool with `UV_TOOL_BIN_DIR` set to
`/usr/local/bin` with the default root user and alternative non-root
users to confirm breaking nature of the change.
While reviewing #14107, @oconnor663
pointed out a bug where we allow `uv python pin --rm` to delete the
global pin without the `--global` flag. I think that shouldn't be
allowed? I'm not 100% certain though.
Right now, `--python-platform linux` to defaults to `manylinux_2_17`.
Defaulting to `manylinux_2_17` causes some problems for users, since it
means we can't use (e.g.) `manylinux_2_28` wheels, and end up having to
build from source.

cibuildwheel made `manylinux_2_28` their default in
pypa/cibuildwheel#1988, and there's a lot of
discussion in pypa/cibuildwheel#1772 and
pypa/cibuildwheel#2047. In short, the
`manylinux_2014` image is EOL, and the vast majority of consumers now
run at least glibc 2.28 (https://mayeut.github.io/manylinux-timeline/):

![Screenshot 2025-06-26 at 7 47
23 PM](https://github.com/user-attachments/assets/2672d91b-f9eb-4442-b680-7e4cd7cade91)

Note that this only changes the _default_. Users can still compile
against `manylinux_2_17` by specifying it.
If `--workspace` is provided, we add all paths as workspace members.

If `--no-workspace` is provided, we add all paths as direct path
dependencies.

If neither is provided, then we add any paths that are under the
workspace root as workspace members, and the rest as direct path
dependencies.

Closes #14524.
If a user specifies `-e /path/to/dir` and `/path/to/dir` in a `uv pip
install` command, we want the editable to "win" (rather than erroring
due to conflicting URLs). Unfortunately, this behavior meant that when
you requested a package as editable and non-editable in conflicting
groups, the editable version was _always_ used. This PR modifies the
requisite types to use `Option<bool>` rather than `bool` for the
`editable` field, so we can determine whether a requirement was
explicitly requested as editable, explicitly requested as non-editable,
or not specified (as in the case of `/path/to/dir` in a
`requirements.txt` file). In the latter case, we allow editables to
override the "unspecified" requirement.

If a project includes a path dependency twice, once with `editable =
true` and once without any `editable` annotation, those are now
considered conflicting URLs, and lead to an error, so I've marked this
change as breaking.

Closes #14139.
This has some changes to the user-facing output, but makes it more
consistent with the rest of uv.
We weren't following our usual "destructure all the options" pattern in
this function, and several "this isn't actually read from uv.toml"
fields slipped through the cracks over time since folks forgot it
existed.

Fixes part of #14308, although we could still try to make the warning in
FilesystemOptions more accurate?

You could argue this is a breaking change, but I think it ultimately
isn't really, because we were already silently ignoring these fields.
Now we properly error.
This PR creates separation between the `--with` environment and the
environment we actually run in, which in turn solves issues like
#12889 whereby two invocations
share the same `--with` environment, causing them to collide by way of
sharing an overlay.

Closes #7643.
In the case of `uv sync` all we really need to do is handle the
`OutdatedEnvironment` error (precisely the error we yield only on
dry-runs when everything Works but we determine things are outdated) in
`OperationDiagnostic::report` (the post-processor on all
`operations::install` calls) because any diagnostic handled by that gets
downgraded to from status 2 to status 1 (although I don't know if that's
really intentional or a random other bug in our status handling... but I
figured it's best to highlight that other potential status code
incongruence than not rely on it 😄).

Fixes #12603

---------

Co-authored-by: John Mumm <jtfmumm@gmail.com>
Fixes #14157

---------

Co-authored-by: John Mumm <jtfmumm@gmail.com>
…`) (#14661)

Closes #14298

Switch the default build backend for `uv init` from `hatchling` to
`uv_build`.

This change affects the following two commands:

* `uv init --lib`
* `uv init [--app] --package`

It does not affect `uv init` or `uv init --app` without `--package`. `uv
init --build-backend <...>` also works as before.

**Before**

```
$ uv init --lib project
$ cat project/pyproject.toml
[project]
name = "project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
    { name = "konstin", email = "konstin@mailbox.org" }
]
requires-python = ">=3.13.2"
dependencies = []

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

**After**

```
$ uv init --lib project
$ cat project/pyproject.toml
[project]
name = "project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
    { name = "konstin", email = "konstin@mailbox.org" }
]
requires-python = ">=3.13.2"
dependencies = []

[build-system]
requires = ["uv_build>=0.7.20,<0.8"]
build-backend = "uv_build"
```

I cleaned up some tests for consistency in the second commit.
Following #14614 this is non-fatal and has an opt-out so it should be
safe to stabilize.
By default, `uv venv <venv-name>` currently removes the `<venv-name`>
directory if it exists. This can be surprising behavior: not everyone
expects an existing environment to be overwritten. This PR updates the
default to fail if a non-empty `<venv-name>` directory already exists
and neither `--allow-existing` nor the new `-c/--clear` option is
provided (if a TTY is detected, it prompts first). If it's not a TTY,
then uv will only warn and not fail for now — we'll make this an error
in the future. I've also added a corresponding `UV_VENV_CLEAR` env var.

I've chosen to use `--clear` instead of `--force` for this option
because it is used by the `venv` module and `virtualenv` and will be
familiar to users. I also think its meaning is clearer in this context
than `--force` (which could plausibly mean force overwrite just the
virtual environment files, which is what our current `--allow-existing`
option does).

Closes #1472.

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
Closes #5144

e.g.

```
❯ cargo run -q -- sync --python-preference only-system
Using CPython 3.12.6 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12
Removed virtual environment at: .venv
Creating virtual environment at: .venv
Resolved 9 packages in 14ms
Installed 8 packages in 9ms
 + anyio==4.6.0
 + certifi==2024.8.30
 + h11==0.14.0
 + httpcore==1.0.5
 + httpx==0.27.2
 + idna==3.10
 + ruff==0.6.7
 + sniffio==1.3.1
 
❯ cargo run -q -- sync --python-preference only-managed
Using CPython 3.12.1
Removed virtual environment at: .venv
Creating virtual environment at: .venv
Resolved 9 packages in 14ms
Installed 8 packages in 11ms
 + anyio==4.6.0
 + certifi==2024.8.30
 + h11==0.14.0
 + httpcore==1.0.5
 + httpx==0.27.2
 + idna==3.10
 + ruff==0.6.7
 + sniffio==1.3.1
```
We currently treat path sources as virtual if they do not specify a
build system, which is surprising behavior. This PR updates the behavior
to treat path sources as packages unless the path source is explicitly
marked as `package = false` or its own `tool.uv.package` is set to
`false`.

Closes #12015

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
@jtfmumm jtfmumm added the do-not-merge Pull request is not ready to merge label Jul 17, 2025
@jtfmumm jtfmumm temporarily deployed to uv-test-registries July 17, 2025 15:35 — with GitHub Actions Inactive
}

for (package_name, workspace_member_project) in &member_projects {
if let Some(dependencies) = &workspace_member_project.project.dependencies {
Copy link
Member

Choose a reason for hiding this comment

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

You need to handle optional dependencies and dependency groups, right?

},
)
})
.collect::<BTreeMap<PackageName, WorkspaceMember>>()
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this type annotation redundant given the return signature?

fn members_from_member_projects(
member_projects: BTreeMap<PackageName, WorkspaceMemberProject>,
) -> BTreeMap<PackageName, WorkspaceMember> {
let mut graph = DiGraph::new();
Copy link
Member

Choose a reason for hiding this comment

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

What is the graph getting us here? Why can't we just insert directly into a BTreeMap?

if let Some(dependencies) = &workspace_member_project.project.dependencies {
let source_node = node_indices[package_name];

for dependency in dependencies {
Copy link
Member

Choose a reason for hiding this comment

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

I think we're going to want to omit self dependencies, e.g., for add_self

#[cfg_attr(test, derive(serde::Serialize))]
pub struct WorkspaceMember {
/// FIXME
project: WorkspaceMemberProject,
Copy link
Member

Choose a reason for hiding this comment

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

nit: It's a little weird to have project.project, e.g., to access the inner Project. It hints the naming might not be quite right.

Copy link
Member

Choose a reason for hiding this comment

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

Would be resolved by #14683 (comment)

#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct WorkspaceMember {
struct WorkspaceMemberProject {
Copy link
Member

Choose a reason for hiding this comment

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

This type only exists for collect_members_only, right? Could we just declare this type locally and keep the public WorkspaceMember flat?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge Pull request is not ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants