Skip to content

Commit

Permalink
Support uv build --wheel from source distributions
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Sep 4, 2024
1 parent c2536ad commit 99745b0
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 48 deletions.
20 changes: 14 additions & 6 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,14 +341,20 @@ pub enum Commands {
Venv(VenvArgs),
/// Build Python packages into source distributions and wheels.
///
/// By default, `uv build` will build a source distribution ("sdist")
/// from the source directory, and a binary distribution ("wheel") from
/// the source distribution.
/// `uv build` accepts a path to a directory or source distribution,
/// which defaults to the current working directory.
///
/// By default, if passed a directory, `uv build` will build a source
/// distribution ("sdist") from the source directory, and a binary
/// distribution ("wheel") from the source distribution.
///
/// `uv build --sdist` can be used to build only the source distribution,
/// `uv build --wheel` can be used to build only the binary distribution,
/// and `uv build --sdist --wheel` can be used to build both distributions
/// from source.
///
/// If passed a source distribution, `uv build --wheel` will build a wheel
/// from the source distribution.
#[command(
after_help = "Use `uv help build` for more details.",
after_long_help = ""
Expand Down Expand Up @@ -1942,15 +1948,17 @@ pub struct PipTreeArgs {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct BuildArgs {
/// The directory from which distributions should be built.
/// The directory from which distributions should be built, or a source
/// distribution archive to build into a wheel.
///
/// Defaults to the current working directory.
#[arg(value_parser = parse_file_path)]
pub src_dir: Option<PathBuf>,
pub src: Option<PathBuf>,

/// The output directory to which distributions should be written.
///
/// Defaults to the `dist` subdirectory within the source directory.
/// Defaults to the `dist` subdirectory within the source directory, or the
/// directory containing the source distribution archive.
#[arg(long, short, value_parser = parse_file_path)]
pub out_dir: Option<PathBuf>,

Expand Down
164 changes: 130 additions & 34 deletions crates/uv/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError};
/// Build source distributions and wheels.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn build(
src_dir: Option<PathBuf>,
src: Option<PathBuf>,
output_dir: Option<PathBuf>,
sdist: bool,
wheel: bool,
Expand All @@ -43,7 +43,7 @@ pub(crate) async fn build(
printer: Printer,
) -> Result<ExitStatus> {
let assets = build_impl(
src_dir.as_deref(),
src.as_deref(),
output_dir.as_deref(),
sdist,
wheel,
Expand Down Expand Up @@ -81,7 +81,7 @@ pub(crate) async fn build(

#[allow(clippy::fn_params_excessive_bools)]
async fn build_impl(
src_dir: Option<&Path>,
src: Option<&Path>,
output_dir: Option<&Path>,
sdist: bool,
wheel: bool,
Expand Down Expand Up @@ -118,41 +118,63 @@ async fn build_impl(
.connectivity(connectivity)
.native_tls(native_tls);

let src_dir = if let Some(src_dir) = src_dir {
Cow::Owned(std::path::absolute(src_dir)?)
let src = if let Some(src) = src {
let src = std::path::absolute(src)?;
let metadata = match fs_err::tokio::metadata(&src).await {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(anyhow::anyhow!(
"Source `{}` does not exist",
src.user_display()
));
}
Err(err) => return Err(err.into()),
};
if metadata.is_file() {
Source::File(Cow::Owned(src))
} else {
Source::Directory(Cow::Owned(src))
}
} else {
Cow::Borrowed(&*CWD)
Source::Directory(Cow::Borrowed(&*CWD))
};

let src_dir = match src {
Source::Directory(ref src) => src,
Source::File(ref src) => src.parent().unwrap(),
};

let output_dir = if let Some(output_dir) = output_dir {
std::path::absolute(output_dir)?
Cow::Owned(std::path::absolute(output_dir)?)
} else {
src_dir.join("dist")
match src {
Source::Directory(ref src) => Cow::Owned(src.join("dist")),
Source::File(ref src) => Cow::Borrowed(src.parent().unwrap()),
}
};

// (1) Explicit request from user
let mut interpreter_request = python_request.map(PythonRequest::parse);

// (2) Request from `.python-version`
if interpreter_request.is_none() {
interpreter_request = PythonVersionFile::discover(src_dir.as_ref(), no_config, false)
interpreter_request = PythonVersionFile::discover(&src_dir, 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.as_ref(), &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
}
};
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())?
Expand Down Expand Up @@ -242,27 +264,49 @@ async fn build_impl(
concurrency,
);

// Create the output directory.
fs_err::tokio::create_dir_all(&output_dir).await?;

// Determine the build plan from the command-line arguments.
let plan = match (sdist, wheel) {
(false, false) => BuildPlan::SdistToWheel,
(true, false) => BuildPlan::Sdist,
(false, true) => BuildPlan::Wheel,
(true, true) => BuildPlan::SdistAndWheel,
// Determine the build plan.
let plan = match &src {
Source::File(_) => {
// We're building from a file, which must be a source distribution.
match (sdist, wheel) {
(false, true) => BuildPlan::WheelFromSdist,
(false, false) => {
return Err(anyhow::anyhow!(
"Pass `--wheel` explicitly to build a wheel from a source distribution"
));
}
(true, _) => {
return Err(anyhow::anyhow!(
"Building an `--sdist` from a source distribution is not supported"
));
}
}
}
Source::Directory(_) => {
// We're building from a directory.
match (sdist, wheel) {
(false, false) => BuildPlan::SdistToWheel,
(false, true) => BuildPlan::Wheel,
(true, false) => BuildPlan::Sdist,
(true, true) => BuildPlan::SdistAndWheel,
}
}
};

// Prepare some common arguments for the build.
let subdirectory = None;
let version_id = src_dir.file_name().unwrap().to_string_lossy();
let version_id = src.path().file_name().unwrap().to_string_lossy();
let dist = None;

let assets = match plan {
BuildPlan::SdistToWheel => {
// Build the sdist.
let builder = build_dispatch
.setup_build(
src_dir.as_ref(),
src.path(),
subdirectory,
&version_id,
dist,
Expand All @@ -274,7 +318,9 @@ async fn build_impl(
// Extract the source distribution into a temporary directory.
let path = output_dir.join(&sdist);
let reader = fs_err::tokio::File::open(&path).await?;
let ext = SourceDistExtension::from_path(&path)?;
let ext = SourceDistExtension::from_path(path.as_path()).map_err(|err| {
anyhow::anyhow!("`{}` is not a valid source distribution, as it ends with an unsupported extension. Expected one of: {err}.", path.user_display())
})?;
let temp_dir = tempfile::tempdir_in(&output_dir)?;
uv_extract::stream::archive(reader, ext, temp_dir.path()).await?;

Expand Down Expand Up @@ -302,7 +348,7 @@ async fn build_impl(
BuildPlan::Sdist => {
let builder = build_dispatch
.setup_build(
src_dir.as_ref(),
src.path(),
subdirectory,
&version_id,
dist,
Expand All @@ -316,7 +362,7 @@ async fn build_impl(
BuildPlan::Wheel => {
let builder = build_dispatch
.setup_build(
src_dir.as_ref(),
src.path(),
subdirectory,
&version_id,
dist,
Expand All @@ -330,7 +376,7 @@ async fn build_impl(
BuildPlan::SdistAndWheel => {
let builder = build_dispatch
.setup_build(
src_dir.as_ref(),
src.path(),
subdirectory,
&version_id,
dist,
Expand All @@ -341,7 +387,7 @@ async fn build_impl(

let builder = build_dispatch
.setup_build(
src_dir.as_ref(),
src.path(),
subdirectory,
&version_id,
dist,
Expand All @@ -352,12 +398,59 @@ async fn build_impl(

BuiltDistributions::Both(output_dir.join(&sdist), output_dir.join(&wheel))
}
BuildPlan::WheelFromSdist => {
// Extract the source distribution into a temporary directory.
let reader = fs_err::tokio::File::open(src.path()).await?;
let ext = SourceDistExtension::from_path(src.path()).map_err(|err| {
anyhow::anyhow!("`{}` is not a valid build source. Expected to receive a source directory, or a source distribution ending in one of: {err}.", src.path().user_display())
})?;
let temp_dir = tempfile::tempdir_in(&output_dir)?;
uv_extract::stream::archive(reader, ext, temp_dir.path()).await?;

// Extract the top-level directory from the archive.
let extracted = match uv_extract::strip_component(temp_dir.path()) {
Ok(top_level) => top_level,
Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.path().to_path_buf(),
Err(err) => return Err(err.into()),
};

// Build a wheel from the source distribution.
let builder = build_dispatch
.setup_build(
&extracted,
subdirectory,
&version_id,
dist,
BuildKind::Wheel,
)
.await?;
let wheel = builder.build(&output_dir).await?;

BuiltDistributions::Wheel(output_dir.join(wheel))
}
};

Ok(assets)
}

#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
enum Source<'a> {
/// The input source is a file (i.e., a source distribution in a `.tar.gz` or `.zip` file).
File(Cow<'a, Path>),
/// The input source is a directory.
Directory(Cow<'a, Path>),
}

impl<'a> Source<'a> {
fn path(&self) -> &Path {
match self {
Source::File(path) => path.as_ref(),
Source::Directory(path) => path.as_ref(),
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum BuiltDistributions {
/// A built wheel.
Wheel(PathBuf),
Expand All @@ -380,4 +473,7 @@ enum BuildPlan {

/// Build a source distribution and a wheel from source.
SdistAndWheel,

/// Build a wheel from a source distribution.
WheelFromSdist,
}
2 changes: 1 addition & 1 deletion crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
);

commands::build(
args.src_dir,
args.src,
args.out_dir,
args.sdist,
args.wheel,
Expand Down
6 changes: 3 additions & 3 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1615,7 +1615,7 @@ impl PipCheckSettings {
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct BuildSettings {
pub(crate) src_dir: Option<PathBuf>,
pub(crate) src: Option<PathBuf>,
pub(crate) out_dir: Option<PathBuf>,
pub(crate) sdist: bool,
pub(crate) wheel: bool,
Expand All @@ -1628,7 +1628,7 @@ impl BuildSettings {
/// Resolve the [`BuildSettings`] from the CLI and filesystem configuration.
pub(crate) fn resolve(args: BuildArgs, filesystem: Option<FilesystemOptions>) -> Self {
let BuildArgs {
src_dir,
src,
out_dir,
sdist,
wheel,
Expand All @@ -1639,7 +1639,7 @@ impl BuildSettings {
} = args;

Self {
src_dir,
src,
out_dir,
sdist,
wheel,
Expand Down
Loading

0 comments on commit 99745b0

Please sign in to comment.