From c3d7d3899c435d528d34f242a3750aeed1bb8c50 Mon Sep 17 00:00:00 2001
From: Charlie Marsh
Date: Thu, 10 Jul 2025 22:05:49 -0400
Subject: [PATCH] Default to `--workspace` when adding subdirectories (#14529)
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.
---
crates/uv-cli/src/lib.rs | 15 +-
crates/uv/src/commands/project/add.rs | 80 +++--
crates/uv/src/settings.rs | 5 +-
crates/uv/tests/it/edit.rs | 454 +++++++++++++++++++++++++-
docs/reference/cli.md | 10 +-
5 files changed, 522 insertions(+), 42 deletions(-)
diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs
index 94b79558d..4c01fd780 100644
--- a/crates/uv-cli/src/lib.rs
+++ b/crates/uv-cli/src/lib.rs
@@ -3726,10 +3726,19 @@ pub struct AddArgs {
/// Add the dependency as a workspace member.
///
- /// When used with a path dependency, the package will be added to the workspace's `members`
- /// list in the root `pyproject.toml` file.
- #[arg(long)]
+ /// By default, uv will add path dependencies that are within the workspace directory
+ /// as workspace members. When used with a path dependency, the package will be added
+ /// to the workspace's `members` list in the root `pyproject.toml` file.
+ #[arg(long, overrides_with = "no_workspace")]
pub workspace: bool,
+
+ /// Don't add the dependency as a workspace member.
+ ///
+ /// By default, when adding a dependency that's a local path and is within the workspace
+ /// directory, uv will add it as a workspace member; pass `--no-workspace` to add the package
+ /// as direct path dependency instead.
+ #[arg(long, overrides_with = "workspace")]
+ pub no_workspace: bool,
}
#[derive(Args)]
diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs
index d65866483..28cc2dcd5 100644
--- a/crates/uv/src/commands/project/add.rs
+++ b/crates/uv/src/commands/project/add.rs
@@ -83,7 +83,7 @@ pub(crate) async fn add(
extras_of_dependency: Vec,
package: Option,
python: Option,
- workspace: bool,
+ workspace: Option,
install_mirrors: PythonInstallMirrors,
settings: ResolverInstallerSettings,
network_settings: NetworkSettings,
@@ -497,16 +497,41 @@ pub(crate) async fn add(
// Track modification status, for reverts.
let mut modified = false;
- // If `--workspace` is provided, add any members to the `workspace` section of the
+ // Determine whether to use workspace mode.
+ let use_workspace = match workspace {
+ Some(workspace) => workspace,
+ None => {
+ // Check if we're in a project (not a script), and if any requirements are path
+ // dependencies within the workspace.
+ if let AddTarget::Project(ref project, _) = target {
+ let workspace_root = project.workspace().install_path();
+ requirements.iter().any(|req| {
+ if let RequirementSource::Directory { install_path, .. } = &req.source {
+ let absolute_path = if install_path.is_absolute() {
+ install_path.to_path_buf()
+ } else {
+ project.root().join(install_path)
+ };
+ absolute_path.starts_with(workspace_root)
+ } else {
+ false
+ }
+ })
+ } else {
+ false
+ }
+ }
+ };
+
+ // If workspace mode is enabled, add any members to the `workspace` section of the
// `pyproject.toml` file.
- if workspace {
+ if use_workspace {
let AddTarget::Project(project, python_target) = target else {
unreachable!("`--workspace` and `--script` are conflicting options");
};
- let workspace = project.workspace();
let mut toml = PyProjectTomlMut::from_toml(
- &workspace.pyproject_toml().raw,
+ &project.workspace().pyproject_toml().raw,
DependencyTarget::PyProjectToml,
)?;
@@ -519,21 +544,32 @@ pub(crate) async fn add(
project.root().join(install_path)
};
- // Check if the path is not already included in the workspace.
- if !workspace.includes(&absolute_path)? {
- let relative_path = absolute_path
- .strip_prefix(workspace.install_path())
- .unwrap_or(&absolute_path);
-
- toml.add_workspace(relative_path)?;
- modified |= true;
-
- writeln!(
- printer.stderr(),
- "Added `{}` to workspace members",
- relative_path.user_display().cyan()
- )?;
+ // Either `--workspace` was provided explicitly, or it was omitted but the path is
+ // within the workspace root.
+ let use_workspace = workspace.unwrap_or_else(|| {
+ absolute_path.starts_with(project.workspace().install_path())
+ });
+ if !use_workspace {
+ continue;
}
+
+ // If the project is already a member of the workspace, skip it.
+ if project.workspace().includes(&absolute_path)? {
+ continue;
+ }
+
+ let relative_path = absolute_path
+ .strip_prefix(project.workspace().install_path())
+ .unwrap_or(&absolute_path);
+
+ toml.add_workspace(relative_path)?;
+ modified |= true;
+
+ writeln!(
+ printer.stderr(),
+ "Added `{}` to workspace members",
+ relative_path.user_display().cyan()
+ )?;
}
}
@@ -542,7 +578,7 @@ pub(crate) async fn add(
target = if modified {
let workspace_content = toml.to_string();
fs_err::write(
- workspace.install_path().join("pyproject.toml"),
+ project.workspace().install_path().join("pyproject.toml"),
&workspace_content,
)?;
@@ -747,13 +783,13 @@ fn edits(
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.sources.as_ref())
.map(ToolUvSources::inner);
- let workspace = project
+ let is_workspace_member = project
.workspace()
.packages()
.contains_key(&requirement.name);
resolve_requirement(
requirement,
- workspace,
+ is_workspace_member,
editable,
index.cloned(),
rev.map(ToString::to_string),
diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs
index b246f228f..bf3bca4a4 100644
--- a/crates/uv/src/settings.rs
+++ b/crates/uv/src/settings.rs
@@ -1351,7 +1351,7 @@ pub(crate) struct AddSettings {
pub(crate) package: Option,
pub(crate) script: Option,
pub(crate) python: Option,
- pub(crate) workspace: bool,
+ pub(crate) workspace: Option,
pub(crate) install_mirrors: PythonInstallMirrors,
pub(crate) refresh: Refresh,
pub(crate) indexes: Vec,
@@ -1390,6 +1390,7 @@ impl AddSettings {
script,
python,
workspace,
+ no_workspace,
} = args;
let dependency_type = if let Some(extra) = optional {
@@ -1490,7 +1491,7 @@ impl AddSettings {
package,
script,
python: python.and_then(Maybe::into_option),
- workspace,
+ workspace: flag(workspace, no_workspace, "workspace"),
editable: flag(editable, no_editable, "editable"),
extras: extra.unwrap_or_default(),
refresh: Refresh::from(refresh),
diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs
index ddaed434f..ccc0cabf2 100644
--- a/crates/uv/tests/it/edit.rs
+++ b/crates/uv/tests/it/edit.rs
@@ -2491,9 +2491,9 @@ fn add_workspace_path() -> Result<()> {
Ok(())
}
-/// Add a path dependency.
+/// Add a path dependency, which should be implicitly added to the workspace.
#[test]
-fn add_path() -> Result<()> {
+fn add_path_implicit_workspace() -> Result<()> {
let context = TestContext::new("3.12");
let workspace = context.temp_dir.child("workspace");
@@ -2533,6 +2533,7 @@ fn add_path() -> Result<()> {
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
+ Added `packages/child` to workspace members
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
@@ -2545,7 +2546,134 @@ fn add_path() -> Result<()> {
filters => context.filters(),
}, {
assert_snapshot!(
- pyproject_toml, @r###"
+ pyproject_toml, @r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = [
+ "child",
+ ]
+
+ [tool.uv.workspace]
+ members = [
+ "packages/child",
+ ]
+
+ [tool.uv.sources]
+ child = { workspace = true }
+ "#
+ );
+ });
+
+ // `uv add` implies a full lock and sync, including development dependencies.
+ let lock = fs_err::read_to_string(workspace.join("uv.lock"))?;
+
+ insta::with_settings!({
+ filters => context.filters(),
+ }, {
+ assert_snapshot!(
+ lock, @r#"
+ version = 1
+ revision = 2
+ requires-python = ">=3.12"
+
+ [options]
+ exclude-newer = "2024-03-25T00:00:00Z"
+
+ [manifest]
+ members = [
+ "child",
+ "parent",
+ ]
+
+ [[package]]
+ name = "child"
+ version = "0.1.0"
+ source = { editable = "packages/child" }
+
+ [[package]]
+ name = "parent"
+ version = "0.1.0"
+ source = { virtual = "." }
+ dependencies = [
+ { name = "child" },
+ ]
+
+ [package.metadata]
+ requires-dist = [{ name = "child", editable = "packages/child" }]
+ "#
+ );
+ });
+
+ // Install from the lockfile.
+ uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(workspace.path()), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Audited 1 package in [TIME]
+ ");
+
+ Ok(())
+}
+
+/// Add a path dependency with `--no-workspace`, which should not be added to the workspace.
+#[test]
+fn add_path_no_workspace() -> Result<()> {
+ let context = TestContext::new("3.12");
+
+ let workspace = context.temp_dir.child("workspace");
+ workspace.child("pyproject.toml").write_str(indoc! {r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = []
+ "#})?;
+
+ let child = workspace.child("packages").child("child");
+ child.child("pyproject.toml").write_str(indoc! {r#"
+ [project]
+ name = "child"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = []
+
+ [build-system]
+ requires = ["hatchling"]
+ build-backend = "hatchling.build"
+ "#})?;
+ workspace
+ .child("packages")
+ .child("child")
+ .child("src")
+ .child("child")
+ .child("__init__.py")
+ .touch()?;
+
+ uv_snapshot!(context.filters(), context.add().arg(Path::new("packages").join("child")).current_dir(workspace.path()).arg("--no-workspace"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
+ Creating virtual environment at: .venv
+ Resolved 2 packages in [TIME]
+ Prepared 1 package in [TIME]
+ Installed 1 package in [TIME]
+ + child==0.1.0 (from file://[TEMP_DIR]/workspace/packages/child)
+ ");
+
+ let pyproject_toml = fs_err::read_to_string(workspace.join("pyproject.toml"))?;
+
+ insta::with_settings!({
+ filters => context.filters(),
+ }, {
+ assert_snapshot!(
+ pyproject_toml, @r#"
[project]
name = "parent"
version = "0.1.0"
@@ -2556,7 +2684,7 @@ fn add_path() -> Result<()> {
[tool.uv.sources]
child = { path = "packages/child" }
- "###
+ "#
);
});
@@ -2607,6 +2735,110 @@ fn add_path() -> Result<()> {
Ok(())
}
+/// Add a path dependency in an adjacent directory, which should not be added to the workspace.
+#[test]
+fn add_path_adjacent_directory() -> Result<()> {
+ let context = TestContext::new("3.12");
+
+ let project = context.temp_dir.child("project");
+ project.child("pyproject.toml").write_str(indoc! {r#"
+ [project]
+ name = "project"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = []
+ "#})?;
+
+ let dependency = context.temp_dir.child("dependency");
+ dependency.child("pyproject.toml").write_str(indoc! {r#"
+ [project]
+ name = "dependency"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = []
+
+ [build-system]
+ requires = ["hatchling"]
+ build-backend = "hatchling.build"
+ "#})?;
+ dependency
+ .child("src")
+ .child("dependency")
+ .child("__init__.py")
+ .touch()?;
+
+ uv_snapshot!(context.filters(), context.add().arg(dependency.path()).current_dir(project.path()), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
+ Creating virtual environment at: .venv
+ Resolved 2 packages in [TIME]
+ Prepared 1 package in [TIME]
+ Installed 1 package in [TIME]
+ + dependency==0.1.0 (from file://[TEMP_DIR]/dependency)
+ ");
+
+ let pyproject_toml = fs_err::read_to_string(project.join("pyproject.toml"))?;
+
+ insta::with_settings!({
+ filters => context.filters(),
+ }, {
+ assert_snapshot!(
+ pyproject_toml, @r#"
+ [project]
+ name = "project"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = [
+ "dependency",
+ ]
+
+ [tool.uv.sources]
+ dependency = { path = "../dependency" }
+ "#
+ );
+ });
+
+ // `uv add` implies a full lock and sync, including development dependencies.
+ let lock = fs_err::read_to_string(project.join("uv.lock"))?;
+
+ insta::with_settings!({
+ filters => context.filters(),
+ }, {
+ assert_snapshot!(
+ lock, @r#"
+ version = 1
+ revision = 2
+ requires-python = ">=3.12"
+
+ [options]
+ exclude-newer = "2024-03-25T00:00:00Z"
+
+ [[package]]
+ name = "dependency"
+ version = "0.1.0"
+ source = { directory = "../dependency" }
+
+ [[package]]
+ name = "project"
+ version = "0.1.0"
+ source = { virtual = "." }
+ dependencies = [
+ { name = "dependency" },
+ ]
+
+ [package.metadata]
+ requires-dist = [{ name = "dependency", directory = "../dependency" }]
+ "#
+ );
+ });
+
+ Ok(())
+}
+
/// Update a requirement, modifying the source and extras.
#[test]
#[cfg(feature = "git")]
@@ -7249,7 +7481,7 @@ fn fail_to_add_revert_project() -> Result<()> {
.child("setup.py")
.write_str("1/0")?;
- uv_snapshot!(context.filters(), context.add().arg("./child"), @r#"
+ uv_snapshot!(context.filters(), context.add().arg("./child").arg("--no-workspace"), @r#"
success: false
exit_code: 1
----- stdout -----
@@ -7351,7 +7583,7 @@ fn fail_to_edit_revert_project() -> Result<()> {
.child("setup.py")
.write_str("1/0")?;
- uv_snapshot!(context.filters(), context.add().arg("./child"), @r#"
+ uv_snapshot!(context.filters(), context.add().arg("./child").arg("--no-workspace"), @r#"
success: false
exit_code: 1
----- stdout -----
@@ -7460,7 +7692,7 @@ fn fail_to_add_revert_workspace_root() -> Result<()> {
.child("setup.py")
.write_str("1/0")?;
- uv_snapshot!(context.filters(), context.add().arg("--workspace").arg("./broken"), @r#"
+ uv_snapshot!(context.filters(), context.add().arg("./broken"), @r#"
success: false
exit_code: 1
----- stdout -----
@@ -7575,7 +7807,7 @@ fn fail_to_add_revert_workspace_member() -> Result<()> {
.child("setup.py")
.write_str("1/0")?;
- uv_snapshot!(context.filters(), context.add().current_dir(&project).arg("--workspace").arg("../broken"), @r#"
+ uv_snapshot!(context.filters(), context.add().current_dir(&project).arg("../broken"), @r#"
success: false
exit_code: 1
----- stdout -----
@@ -12928,12 +13160,12 @@ fn add_path_with_existing_workspace() -> Result<()> {
dependencies = []
"#})?;
- // Add the dependency with `--workspace` flag from the project directory.
+ // Add the dependency from the project directory. It should automatically be added as a
+ // workspace member, since it's in the same directory as the workspace.
uv_snapshot!(context.filters(), context
.add()
.current_dir(&project_dir)
- .arg("../dep")
- .arg("--workspace"), @r"
+ .arg("../dep"), @r"
success: true
exit_code: 0
----- stdout -----
@@ -13044,3 +13276,203 @@ fn add_path_with_workspace() -> Result<()> {
Ok(())
}
+
+/// Add a path dependency within the workspace directory without --workspace flag.
+/// It should automatically be added as a workspace member.
+#[test]
+fn add_path_within_workspace_defaults_to_workspace() -> Result<()> {
+ let context = TestContext::new("3.12");
+
+ let workspace_toml = context.temp_dir.child("pyproject.toml");
+ workspace_toml.write_str(indoc! {r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = []
+
+ [tool.uv.workspace]
+ members = []
+ "#})?;
+
+ let dep_dir = context.temp_dir.child("dep");
+ dep_dir.child("pyproject.toml").write_str(indoc! {r#"
+ [project]
+ name = "dep"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = []
+ "#})?;
+
+ // Add the dependency without --workspace flag - it should still be added as workspace member
+ // since it's within the workspace directory.
+ uv_snapshot!(context.filters(), context
+ .add()
+ .arg("./dep"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Added `dep` to workspace members
+ Resolved 2 packages in [TIME]
+ Audited in [TIME]
+ ");
+
+ let pyproject_toml = context.read("pyproject.toml");
+ assert_snapshot!(
+ pyproject_toml, @r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = [
+ "dep",
+ ]
+
+ [tool.uv.workspace]
+ members = [
+ "dep",
+ ]
+
+ [tool.uv.sources]
+ dep = { workspace = true }
+ "#
+ );
+
+ Ok(())
+}
+
+/// Add a path dependency within the workspace directory with --no-workspace flag.
+/// It should be added as a direct path dependency.
+#[test]
+fn add_path_with_no_workspace() -> Result<()> {
+ let context = TestContext::new("3.12");
+
+ let workspace_toml = context.temp_dir.child("pyproject.toml");
+ workspace_toml.write_str(indoc! {r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = []
+
+ [tool.uv.workspace]
+ members = []
+ "#})?;
+
+ let dep_dir = context.temp_dir.child("dep");
+ dep_dir.child("pyproject.toml").write_str(indoc! {r#"
+ [project]
+ name = "dep"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = []
+ "#})?;
+
+ // Add the dependency with --no-workspace flag - it should be added as direct path dependency.
+ uv_snapshot!(context.filters(), context
+ .add()
+ .arg("./dep")
+ .arg("--no-workspace"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Resolved 2 packages in [TIME]
+ Audited in [TIME]
+ ");
+
+ let pyproject_toml = context.read("pyproject.toml");
+ assert_snapshot!(
+ pyproject_toml, @r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = [
+ "dep",
+ ]
+
+ [tool.uv.workspace]
+ members = []
+
+ [tool.uv.sources]
+ dep = { path = "dep" }
+ "#
+ );
+
+ Ok(())
+}
+
+/// Add a path dependency outside the workspace directory.
+/// It should be added as a direct path dependency, not a workspace member.
+#[test]
+fn add_path_outside_workspace_no_default() -> Result<()> {
+ let context = TestContext::new("3.12");
+
+ // Create a workspace directory
+ let workspace_dir = context.temp_dir.child("workspace");
+ workspace_dir.create_dir_all()?;
+
+ let workspace_toml = workspace_dir.child("pyproject.toml");
+ workspace_toml.write_str(indoc! {r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = []
+
+ [tool.uv.workspace]
+ members = []
+ "#})?;
+
+ // Create a dependency outside the workspace
+ let dep_dir = context.temp_dir.child("external_dep");
+ dep_dir.child("pyproject.toml").write_str(indoc! {r#"
+ [project]
+ name = "dep"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = []
+ "#})?;
+
+ // Add the dependency without --workspace flag - it should be a direct path dependency
+ // since it's outside the workspace directory.
+ uv_snapshot!(context.filters(), context
+ .add()
+ .current_dir(&workspace_dir)
+ .arg("../external_dep"), @r"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+
+ ----- stderr -----
+ Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
+ Creating virtual environment at: .venv
+ Resolved 2 packages in [TIME]
+ Audited in [TIME]
+ ");
+
+ let pyproject_toml = fs_err::read_to_string(workspace_toml)?;
+ assert_snapshot!(
+ pyproject_toml, @r#"
+ [project]
+ name = "parent"
+ version = "0.1.0"
+ requires-python = ">=3.12"
+ dependencies = [
+ "dep",
+ ]
+
+ [tool.uv.workspace]
+ members = []
+
+ [tool.uv.sources]
+ dep = { path = "../external_dep" }
+ "#
+ );
+
+ Ok(())
+}
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index aa6213eff..881c96697 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -535,7 +535,9 @@ uv add [OPTIONS] >
May also be set with the UV_NO_PROGRESS environment variable.
--no-python-downloadsDisable automatic downloads of Python.
--no-sourcesIgnore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any workspace, Git, URL, or local path sources
--no-syncAvoid syncing the virtual environment
-May also be set with the UV_NO_SYNC environment variable.
--offlineDisable network access.
+May also be set with the UV_NO_SYNC environment variable.
--no-workspaceDon't add the dependency as a workspace member.
+By default, when adding a dependency that's a local path and is within the workspace directory, uv will add it as a workspace member; pass --no-workspace to add the package as direct path dependency instead.
+--offlineDisable network access.
When disabled, uv will only use locally cached data and locally available files.
May also be set with the UV_OFFLINE environment variable.
--optional optionalAdd the requirements to the package's optional dependencies for the specified extra.
The group may then be activated when installing the project with the --extra flag.
@@ -583,7 +585,7 @@ uv add [OPTIONS] >
--verbose, -vUse verbose output.
You can configure fine-grained logging using the RUST_LOG environment variable. (https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives)
--workspaceAdd the dependency as a workspace member.
-When used with a path dependency, the package will be added to the workspace's members list in the root pyproject.toml file.
+By default, uv will add path dependencies that are within the workspace directory as workspace members. When used with a path dependency, the package will be added to the workspace's members list in the root pyproject.toml file.
## uv remove
@@ -1154,10 +1156,10 @@ environment in the project.
macos: An alias for aarch64-apple-darwin, the default target for macOS
x86_64-pc-windows-msvc: A 64-bit x86 Windows target
i686-pc-windows-msvc: A 32-bit x86 Windows target
-x86_64-unknown-linux-gnu: An x86 Linux target. Equivalent to x86_64-manylinux_2_17
+x86_64-unknown-linux-gnu: An x86 Linux target. Equivalent to x86_64-manylinux_2_28
aarch64-apple-darwin: An ARM-based macOS target, as seen on Apple Silicon devices
x86_64-apple-darwin: An x86 macOS target
-aarch64-unknown-linux-gnu: An ARM64 Linux target. Equivalent to aarch64-manylinux_2_17
+aarch64-unknown-linux-gnu: An ARM64 Linux target. Equivalent to aarch64-manylinux_2_28
aarch64-unknown-linux-musl: An ARM64 Linux target
x86_64-unknown-linux-musl: An x86_64 Linux target
x86_64-manylinux2014: An x86_64 target for the manylinux2014 platform. Equivalent to x86_64-manylinux_2_17