mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-30 23:37:24 +00:00
Add --package
support to uv build
(#6990)
## Summary This PR adds `--package` support to `uv build`, such that you can use `--package` from anywhere in a workspace to build any member. If a source directory is provided, we use that as the workspace root. If a file is provided, we error. For now, `uv build` only builds the current package, making it semantically identical to `uv sync`.
This commit is contained in:
parent
05ed4bc11d
commit
7aed94bed2
6 changed files with 321 additions and 21 deletions
|
@ -1955,6 +1955,15 @@ pub struct BuildArgs {
|
|||
#[arg(value_parser = parse_file_path)]
|
||||
pub src: Option<PathBuf>,
|
||||
|
||||
/// Build a specific package in the workspace.
|
||||
///
|
||||
/// The workspace will be discovered from the provided source directory, or the current
|
||||
/// directory if no source directory is provided.
|
||||
///
|
||||
/// If the workspace member does not exist, uv will exit with an error.
|
||||
#[arg(long)]
|
||||
pub package: Option<PackageName>,
|
||||
|
||||
/// The output directory to which distributions should be written.
|
||||
///
|
||||
/// Defaults to the `dist` subdirectory within the source directory, or the
|
||||
|
|
|
@ -15,19 +15,20 @@ use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClient
|
|||
use uv_configuration::{BuildKind, BuildOutput, Concurrency};
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_fs::{Simplified, CWD};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_python::{
|
||||
EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation,
|
||||
PythonPreference, PythonRequest, PythonVersionFile, VersionRequest,
|
||||
};
|
||||
use uv_resolver::{FlatIndex, RequiresPython};
|
||||
use uv_types::{BuildContext, BuildIsolation, HashStrategy};
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError};
|
||||
use uv_workspace::{DiscoveryOptions, Workspace};
|
||||
|
||||
/// Build source distributions and wheels.
|
||||
#[allow(clippy::fn_params_excessive_bools)]
|
||||
pub(crate) async fn build(
|
||||
src: Option<PathBuf>,
|
||||
package: Option<PackageName>,
|
||||
output_dir: Option<PathBuf>,
|
||||
sdist: bool,
|
||||
wheel: bool,
|
||||
|
@ -44,6 +45,7 @@ pub(crate) async fn build(
|
|||
) -> Result<ExitStatus> {
|
||||
let assets = build_impl(
|
||||
src.as_deref(),
|
||||
package.as_ref(),
|
||||
output_dir.as_deref(),
|
||||
sdist,
|
||||
wheel,
|
||||
|
@ -82,6 +84,7 @@ pub(crate) async fn build(
|
|||
#[allow(clippy::fn_params_excessive_bools)]
|
||||
async fn build_impl(
|
||||
src: Option<&Path>,
|
||||
package: Option<&PackageName>,
|
||||
output_dir: Option<&Path>,
|
||||
sdist: bool,
|
||||
wheel: bool,
|
||||
|
@ -118,6 +121,7 @@ async fn build_impl(
|
|||
.connectivity(connectivity)
|
||||
.native_tls(native_tls);
|
||||
|
||||
// Determine the source to build.
|
||||
let src = if let Some(src) = src {
|
||||
let src = std::path::absolute(src)?;
|
||||
let metadata = match fs_err::tokio::metadata(&src).await {
|
||||
|
@ -139,9 +143,37 @@ async fn build_impl(
|
|||
Source::Directory(Cow::Borrowed(&*CWD))
|
||||
};
|
||||
|
||||
let src_dir = match src {
|
||||
Source::Directory(ref src) => src,
|
||||
Source::File(ref src) => src.parent().unwrap(),
|
||||
// Attempt to discover the workspace; on failure, save the error for later.
|
||||
let workspace = Workspace::discover(src.directory(), &DiscoveryOptions::default()).await;
|
||||
|
||||
// If a `--package` was provided, adjust the source directory.
|
||||
let src = if let Some(package) = package {
|
||||
if matches!(src, Source::File(_)) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Cannot specify a `--package` when building from a file"
|
||||
));
|
||||
}
|
||||
|
||||
let workspace = match workspace {
|
||||
Ok(ref workspace) => workspace,
|
||||
Err(err) => {
|
||||
return Err(
|
||||
anyhow::anyhow!("`--package` was provided, but no workspace was found")
|
||||
.context(err),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let project = workspace
|
||||
.packages()
|
||||
.get(package)
|
||||
.ok_or_else(|| anyhow::anyhow!("Package `{}` not found in workspace", package))?
|
||||
.root()
|
||||
.clone();
|
||||
|
||||
Source::Directory(Cow::Owned(project))
|
||||
} else {
|
||||
src
|
||||
};
|
||||
|
||||
let output_dir = if let Some(output_dir) = output_dir {
|
||||
|
@ -158,26 +190,15 @@ async fn build_impl(
|
|||
|
||||
// (2) Request from `.python-version`
|
||||
if interpreter_request.is_none() {
|
||||
interpreter_request = PythonVersionFile::discover(&src_dir, no_config, false)
|
||||
interpreter_request = PythonVersionFile::discover(src.directory(), no_config, false)
|
||||
.await?
|
||||
.and_then(PythonVersionFile::into_version);
|
||||
}
|
||||
|
||||
// (3) `Requires-Python` in `pyproject.toml`
|
||||
if interpreter_request.is_none() {
|
||||
let project = match VirtualProject::discover(src_dir, &DiscoveryOptions::default()).await {
|
||||
Ok(project) => Some(project),
|
||||
Err(WorkspaceError::MissingProject(_)) => None,
|
||||
Err(WorkspaceError::MissingPyprojectToml) => None,
|
||||
Err(WorkspaceError::NonWorkspace(_)) => None,
|
||||
Err(err) => {
|
||||
warn_user_once!("{err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project) = project {
|
||||
interpreter_request = find_requires_python(project.workspace())?
|
||||
if let Ok(ref workspace) = workspace {
|
||||
interpreter_request = find_requires_python(workspace)?
|
||||
.as_ref()
|
||||
.map(RequiresPython::specifiers)
|
||||
.map(|specifiers| {
|
||||
|
@ -463,8 +484,15 @@ enum Source<'a> {
|
|||
impl<'a> Source<'a> {
|
||||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
Source::File(path) => path.as_ref(),
|
||||
Source::Directory(path) => path.as_ref(),
|
||||
Self::File(path) => path.as_ref(),
|
||||
Self::Directory(path) => path.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
fn directory(&self) -> &Path {
|
||||
match self {
|
||||
Self::File(path) => path.parent().unwrap(),
|
||||
Self::Directory(path) => path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -672,6 +672,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
|||
|
||||
commands::build(
|
||||
args.src,
|
||||
args.package,
|
||||
args.out_dir,
|
||||
args.sdist,
|
||||
args.wheel,
|
||||
|
|
|
@ -1616,6 +1616,7 @@ impl PipCheckSettings {
|
|||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct BuildSettings {
|
||||
pub(crate) src: Option<PathBuf>,
|
||||
pub(crate) package: Option<PackageName>,
|
||||
pub(crate) out_dir: Option<PathBuf>,
|
||||
pub(crate) sdist: bool,
|
||||
pub(crate) wheel: bool,
|
||||
|
@ -1630,6 +1631,7 @@ impl BuildSettings {
|
|||
let BuildArgs {
|
||||
src,
|
||||
out_dir,
|
||||
package,
|
||||
sdist,
|
||||
wheel,
|
||||
python,
|
||||
|
@ -1640,6 +1642,7 @@ impl BuildSettings {
|
|||
|
||||
Self {
|
||||
src,
|
||||
package,
|
||||
out_dir,
|
||||
sdist,
|
||||
wheel,
|
||||
|
|
|
@ -893,3 +893,256 @@ fn fail() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let filters = context
|
||||
.filters()
|
||||
.into_iter()
|
||||
.chain([
|
||||
(r"exit code: 1", "exit status: 1"),
|
||||
(r"bdist\.[^/\\\s]+-[^/\\\s]+", "bdist.linux-x86_64"),
|
||||
(r"\\\.", ""),
|
||||
])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let project = context.temp_dir.child("project");
|
||||
|
||||
let pyproject_toml = project.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["anyio==3.7.0"]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["packages/*"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
project.child("src").child("__init__.py").touch()?;
|
||||
project.child("README").touch()?;
|
||||
|
||||
let member = project.child("packages").child("member");
|
||||
fs_err::create_dir_all(member.path())?;
|
||||
|
||||
member.child("pyproject.toml").write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "member"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["iniconfig"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
member.child("src").child("__init__.py").touch()?;
|
||||
member.child("README").touch()?;
|
||||
|
||||
// Build the member.
|
||||
uv_snapshot!(&filters, context.build().arg("--package").arg("member").current_dir(&project), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Building source distribution...
|
||||
running egg_info
|
||||
creating src/member.egg-info
|
||||
writing src/member.egg-info/PKG-INFO
|
||||
writing dependency_links to src/member.egg-info/dependency_links.txt
|
||||
writing requirements to src/member.egg-info/requires.txt
|
||||
writing top-level names to src/member.egg-info/top_level.txt
|
||||
writing manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
reading manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
running sdist
|
||||
running egg_info
|
||||
writing src/member.egg-info/PKG-INFO
|
||||
writing dependency_links to src/member.egg-info/dependency_links.txt
|
||||
writing requirements to src/member.egg-info/requires.txt
|
||||
writing top-level names to src/member.egg-info/top_level.txt
|
||||
reading manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
running check
|
||||
creating member-0.1.0
|
||||
creating member-0.1.0/src
|
||||
creating member-0.1.0/src/member.egg-info
|
||||
copying files to member-0.1.0...
|
||||
copying README -> member-0.1.0
|
||||
copying pyproject.toml -> member-0.1.0
|
||||
copying src/__init__.py -> member-0.1.0/src
|
||||
copying src/member.egg-info/PKG-INFO -> member-0.1.0/src/member.egg-info
|
||||
copying src/member.egg-info/SOURCES.txt -> member-0.1.0/src/member.egg-info
|
||||
copying src/member.egg-info/dependency_links.txt -> member-0.1.0/src/member.egg-info
|
||||
copying src/member.egg-info/requires.txt -> member-0.1.0/src/member.egg-info
|
||||
copying src/member.egg-info/top_level.txt -> member-0.1.0/src/member.egg-info
|
||||
copying src/member.egg-info/SOURCES.txt -> member-0.1.0/src/member.egg-info
|
||||
Writing member-0.1.0/setup.cfg
|
||||
Creating tar archive
|
||||
removing 'member-0.1.0' (and everything under it)
|
||||
Building wheel from source distribution...
|
||||
running egg_info
|
||||
writing src/member.egg-info/PKG-INFO
|
||||
writing dependency_links to src/member.egg-info/dependency_links.txt
|
||||
writing requirements to src/member.egg-info/requires.txt
|
||||
writing top-level names to src/member.egg-info/top_level.txt
|
||||
reading manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
running bdist_wheel
|
||||
running build
|
||||
running build_py
|
||||
creating build
|
||||
creating build/lib
|
||||
copying src/__init__.py -> build/lib
|
||||
running egg_info
|
||||
writing src/member.egg-info/PKG-INFO
|
||||
writing dependency_links to src/member.egg-info/dependency_links.txt
|
||||
writing requirements to src/member.egg-info/requires.txt
|
||||
writing top-level names to src/member.egg-info/top_level.txt
|
||||
reading manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
installing to build/bdist.linux-x86_64/wheel
|
||||
running install
|
||||
running install_lib
|
||||
creating build/bdist.linux-x86_64
|
||||
creating build/bdist.linux-x86_64/wheel
|
||||
copying build/lib/__init__.py -> build/bdist.linux-x86_64/wheel
|
||||
running install_egg_info
|
||||
Copying src/member.egg-info to build/bdist.linux-x86_64/wheel/member-0.1.0-py3.12.egg-info
|
||||
running install_scripts
|
||||
creating build/bdist.linux-x86_64/wheel/member-0.1.0.dist-info/WHEEL
|
||||
creating '[TEMP_DIR]/project/packages/member/dist/[TMP]/wheel' to it
|
||||
adding '__init__.py'
|
||||
adding 'member-0.1.0.dist-info/METADATA'
|
||||
adding 'member-0.1.0.dist-info/WHEEL'
|
||||
adding 'member-0.1.0.dist-info/top_level.txt'
|
||||
adding 'member-0.1.0.dist-info/RECORD'
|
||||
removing build/bdist.linux-x86_64/wheel
|
||||
Successfully built packages/member/dist/member-0.1.0.tar.gz and packages/member/dist/member-0.1.0-py3-none-any.whl
|
||||
"###);
|
||||
|
||||
member
|
||||
.child("dist")
|
||||
.child("member-0.1.0.tar.gz")
|
||||
.assert(predicate::path::is_file());
|
||||
member
|
||||
.child("dist")
|
||||
.child("member-0.1.0-py3-none-any.whl")
|
||||
.assert(predicate::path::is_file());
|
||||
|
||||
// If a source is provided, discover the workspace from the source.
|
||||
uv_snapshot!(&filters, context.build().arg("./project").arg("--package").arg("member"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Building source distribution...
|
||||
running egg_info
|
||||
writing src/member.egg-info/PKG-INFO
|
||||
writing dependency_links to src/member.egg-info/dependency_links.txt
|
||||
writing requirements to src/member.egg-info/requires.txt
|
||||
writing top-level names to src/member.egg-info/top_level.txt
|
||||
reading manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
running sdist
|
||||
running egg_info
|
||||
writing src/member.egg-info/PKG-INFO
|
||||
writing dependency_links to src/member.egg-info/dependency_links.txt
|
||||
writing requirements to src/member.egg-info/requires.txt
|
||||
writing top-level names to src/member.egg-info/top_level.txt
|
||||
reading manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
running check
|
||||
creating member-0.1.0
|
||||
creating member-0.1.0/src
|
||||
creating member-0.1.0/src/member.egg-info
|
||||
copying files to member-0.1.0...
|
||||
copying README -> member-0.1.0
|
||||
copying pyproject.toml -> member-0.1.0
|
||||
copying src/__init__.py -> member-0.1.0/src
|
||||
copying src/member.egg-info/PKG-INFO -> member-0.1.0/src/member.egg-info
|
||||
copying src/member.egg-info/SOURCES.txt -> member-0.1.0/src/member.egg-info
|
||||
copying src/member.egg-info/dependency_links.txt -> member-0.1.0/src/member.egg-info
|
||||
copying src/member.egg-info/requires.txt -> member-0.1.0/src/member.egg-info
|
||||
copying src/member.egg-info/top_level.txt -> member-0.1.0/src/member.egg-info
|
||||
copying src/member.egg-info/SOURCES.txt -> member-0.1.0/src/member.egg-info
|
||||
Writing member-0.1.0/setup.cfg
|
||||
Creating tar archive
|
||||
removing 'member-0.1.0' (and everything under it)
|
||||
Building wheel from source distribution...
|
||||
running egg_info
|
||||
writing src/member.egg-info/PKG-INFO
|
||||
writing dependency_links to src/member.egg-info/dependency_links.txt
|
||||
writing requirements to src/member.egg-info/requires.txt
|
||||
writing top-level names to src/member.egg-info/top_level.txt
|
||||
reading manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
running bdist_wheel
|
||||
running build
|
||||
running build_py
|
||||
creating build
|
||||
creating build/lib
|
||||
copying src/__init__.py -> build/lib
|
||||
running egg_info
|
||||
writing src/member.egg-info/PKG-INFO
|
||||
writing dependency_links to src/member.egg-info/dependency_links.txt
|
||||
writing requirements to src/member.egg-info/requires.txt
|
||||
writing top-level names to src/member.egg-info/top_level.txt
|
||||
reading manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/member.egg-info/SOURCES.txt'
|
||||
installing to build/bdist.linux-x86_64/wheel
|
||||
running install
|
||||
running install_lib
|
||||
creating build/bdist.linux-x86_64
|
||||
creating build/bdist.linux-x86_64/wheel
|
||||
copying build/lib/__init__.py -> build/bdist.linux-x86_64/wheel
|
||||
running install_egg_info
|
||||
Copying src/member.egg-info to build/bdist.linux-x86_64/wheel/member-0.1.0-py3.12.egg-info
|
||||
running install_scripts
|
||||
creating build/bdist.linux-x86_64/wheel/member-0.1.0.dist-info/WHEEL
|
||||
creating '[TEMP_DIR]/project/packages/member/dist/[TMP]/wheel' to it
|
||||
adding '__init__.py'
|
||||
adding 'member-0.1.0.dist-info/METADATA'
|
||||
adding 'member-0.1.0.dist-info/WHEEL'
|
||||
adding 'member-0.1.0.dist-info/top_level.txt'
|
||||
adding 'member-0.1.0.dist-info/RECORD'
|
||||
removing build/bdist.linux-x86_64/wheel
|
||||
Successfully built project/packages/member/dist/member-0.1.0.tar.gz and project/packages/member/dist/member-0.1.0-py3-none-any.whl
|
||||
"###);
|
||||
|
||||
// Fail when `--package` is provided without a workspace.
|
||||
uv_snapshot!(&filters, context.build().arg("--package").arg("member"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No `pyproject.toml` found in current directory or any parent directory
|
||||
Caused by: `--package` was provided, but no workspace was found
|
||||
"###);
|
||||
|
||||
// Fail when `--package` is a non-existent member without a workspace.
|
||||
uv_snapshot!(&filters, context.build().arg("--package").arg("fail").current_dir(&project), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Package `fail` not found in workspace
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -6374,6 +6374,12 @@ uv build [OPTIONS] [SRC]
|
|||
|
||||
<p>Defaults to the <code>dist</code> subdirectory within the source directory, or the directory containing the source distribution archive.</p>
|
||||
|
||||
</dd><dt><code>--package</code> <i>package</i></dt><dd><p>Build a specific package in the workspace.</p>
|
||||
|
||||
<p>The workspace will be discovered from the provided source directory, or the current directory if no source directory is provided.</p>
|
||||
|
||||
<p>If the workspace member does not exist, uv will exit with an error.</p>
|
||||
|
||||
</dd><dt><code>--prerelease</code> <i>prerelease</i></dt><dd><p>The strategy to use when considering pre-release versions.</p>
|
||||
|
||||
<p>By default, uv will accept pre-releases for packages that <em>only</em> publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (<code>if-necessary-or-explicit</code>).</p>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue