Default to PEP 517-based builds (#843)

## Summary

Our current setup uses the legacy `setup.py`-based builds if a
`pyproject.toml` file isn't present. This matches pip's behavior.
However, `pypa/build` uses PEP 517-based builds in such cases, and it
looks like pip plans to make that the default
(https://github.com/pypa/pip/issues/9175), with the limiting factor
being performance issues related to isolated builds.

This is now the default behavior, but the `--legacy-setup-py` flag
allows users to opt-in to using `setup.py` directly for distributions
that lack a `pyproject.toml`.
This commit is contained in:
Charlie Marsh
2024-01-09 20:27:06 -05:00
committed by GitHub
parent e26dc8e33d
commit 55f2be72e2
14 changed files with 342 additions and 77 deletions
+98 -67
View File
@@ -11,9 +11,7 @@ use std::process::Output;
use std::str::FromStr;
use std::sync::Arc;
use distribution_types::Resolution;
use fs_err as fs;
use fs_err::DirEntry;
use indoc::formatdoc;
use itertools::Itertools;
use once_cell::sync::Lazy;
@@ -26,10 +24,11 @@ use tokio::process::Command;
use tokio::sync::Mutex;
use tracing::{debug, info_span, instrument, Instrument};
use distribution_types::Resolution;
use pep508_rs::Requirement;
use puffin_extract::extract_source;
use puffin_interpreter::{Interpreter, Virtualenv};
use puffin_traits::{BuildContext, BuildKind, SourceBuildTrait};
use puffin_traits::{BuildContext, BuildKind, SetupPyStrategy, SourceBuildTrait};
/// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory`
static MISSING_HEADER_RE: Lazy<Regex> = Lazy::new(|| {
@@ -38,11 +37,22 @@ static MISSING_HEADER_RE: Lazy<Regex> = Lazy::new(|| {
)
.unwrap()
});
/// e.g. `/usr/bin/ld: cannot find -lncurses: No such file or directory`
static LD_NOT_FOUND_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"/usr/bin/ld: cannot find -l([a-zA-Z10-9]+): No such file or directory").unwrap()
});
/// The default backend to use when PEP 517 is used without a `build-system` section.
static DEFAULT_BACKEND: Lazy<Pep517Backend> = Lazy::new(|| Pep517Backend {
backend: "setuptools.build_meta:__legacy__".to_string(),
backend_path: None,
requirements: vec![
Requirement::from_str("wheel").unwrap(),
Requirement::from_str("setuptools >= 40.8.0").unwrap(),
],
});
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
@@ -89,8 +99,6 @@ pub enum MissingLibrary {
#[derive(Debug, Error)]
pub struct MissingHeaderCause {
missing_library: MissingLibrary,
// I've picked this over the better readable package name to make clear that you need to
// look for the build dependencies of that version or git commit respectively
package_id: String,
}
@@ -109,7 +117,7 @@ impl Display for MissingHeaderCause {
f,
"This error likely indicates that you need to install the library that provides a shared library \
for {library} for {package_id} (e.g. lib{library}-dev)",
library=library, package_id=self.package_id
library = library, package_id = self.package_id
)
}
}
@@ -171,7 +179,7 @@ pub struct PyProjectToml {
}
/// `[build-backend]` from pyproject.toml
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq, Eq)]
struct Pep517Backend {
/// The build backend string such as `setuptools.build_meta:__legacy__` or `maturin` from
/// `build-backend.backend` in pyproject.toml
@@ -222,7 +230,7 @@ impl Pep517Backend {
#[derive(Debug, Default, Clone)]
pub struct SourceBuildContext {
/// Cache the first resolution of `pip`, `setuptools` and `wheel` we made for setup.py (and
/// some PEP 517) builds so we can reuse it
/// some PEP 517) builds so we can reuse it.
setup_py_resolution: Arc<Mutex<Option<Resolution>>>,
}
@@ -234,8 +242,9 @@ pub struct SourceBuildContext {
pub struct SourceBuild {
temp_dir: TempDir,
source_tree: PathBuf,
/// `Some` if this is a PEP 517 build
/// If performing a PEP 517 build, the backend to use.
pep517_backend: Option<Pep517Backend>,
/// The virtual environment in which to build the source distribution.
venv: Virtualenv,
/// Populated if `prepare_metadata_for_build_wheel` was called.
///
@@ -258,6 +267,7 @@ impl SourceBuild {
/// contents from an archive if necessary.
///
/// `source_dist` is for error reporting only.
#[allow(clippy::too_many_arguments)]
pub async fn setup(
source: &Path,
subdirectory: Option<&Path>,
@@ -265,6 +275,7 @@ impl SourceBuild {
build_context: &impl BuildContext,
source_build_context: SourceBuildContext,
package_id: String,
setup_py: SetupPyStrategy,
build_kind: BuildKind,
) -> Result<SourceBuild, Error> {
let temp_dir = tempdir()?;
@@ -283,6 +294,8 @@ impl SourceBuild {
source_root
};
let default_backend: Pep517Backend = DEFAULT_BACKEND.clone();
// Check if we have a PEP 517 build backend.
let pep517_backend = match fs::read_to_string(source_tree.join("pyproject.toml")) {
Ok(toml) => {
@@ -306,74 +319,91 @@ impl SourceBuild {
} else {
// If a `pyproject.toml` is present, but `[build-system]` is missing, proceed with
// a PEP 517 build using the default backend, to match `pip` and `build`.
Some(Pep517Backend {
backend: "setuptools.build_meta:__legacy__".to_string(),
backend_path: None,
requirements: vec![
Requirement::from_str("wheel").unwrap(),
Requirement::from_str("setuptools >= 40.8.0").unwrap(),
],
})
Some(default_backend.clone())
}
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
// We require either a `pyproject.toml` or a `setup.py` file at the top level.
if !source_tree.join("setup.py").is_file() {
return Err(Error::InvalidSourceDist(
"The archive contains neither a `pyproject.toml` nor a `setup.py` file at the top level"
.to_string(),
));
}
// If no `pyproject.toml` is present, by default, proceed with a PEP 517 build using
// the default backend, to match `build`. `pip` uses `setup.py` directly in this
// case (which we allow via `SetupPyStrategy::Setuptools`), but plans to make PEP
// 517 builds the default in the future.
// See: https://github.com/pypa/pip/issues/9175.
match setup_py {
SetupPyStrategy::Pep517 => Some(default_backend.clone()),
SetupPyStrategy::Setuptools => None,
}
}
Err(err) if err.kind() == io::ErrorKind::NotFound => None,
Err(err) => return Err(err.into()),
};
let venv = gourgeist::create_venv(&temp_dir.path().join(".venv"), interpreter.clone())?;
// Setup the build environment using PEP 517 or the legacy setuptools backend.
if let Some(pep517_backend) = pep517_backend.as_ref() {
let resolved_requirements = build_context
.resolve(&pep517_backend.requirements)
.await
.map_err(|err| {
Error::RequirementsInstall("build-system.requires (resolve)", err)
})?;
build_context
.install(&resolved_requirements, &venv)
.await
.map_err(|err| {
Error::RequirementsInstall("build-system.requires (install)", err)
})?;
// Setup the build environment.
let resolved_requirements = if let Some(pep517_backend) = pep517_backend.as_ref() {
if pep517_backend.requirements == default_backend.requirements {
let mut resolution = source_build_context.setup_py_resolution.lock().await;
if let Some(resolved_requirements) = &*resolution {
resolved_requirements.clone()
} else {
let resolved_requirements = build_context
.resolve(&default_backend.requirements)
.await
.map_err(|err| {
Error::RequirementsInstall("setup.py build (resolve)", err)
})?;
*resolution = Some(resolved_requirements.clone());
resolved_requirements
}
} else {
build_context
.resolve(&pep517_backend.requirements)
.await
.map_err(|err| {
Error::RequirementsInstall("build-system.requires (resolve)", err)
})?
}
} else {
let requirements = vec![
Requirement::from_str("wheel").unwrap(),
Requirement::from_str("setuptools").unwrap(),
Requirement::from_str("pip").unwrap(),
];
// Install default requirements for `setup.py`-based builds.
let mut resolution = source_build_context.setup_py_resolution.lock().await;
let resolved_requirements = if let Some(resolved_requirements) = &*resolution {
if let Some(resolved_requirements) = &*resolution {
resolved_requirements.clone()
} else {
let resolved_requirements = build_context
.resolve(&requirements)
.resolve(&default_backend.requirements)
.await
.map_err(|err| Error::RequirementsInstall("setup.py build (resolve)", err))?;
*resolution = Some(resolved_requirements.clone());
resolved_requirements
};
build_context
.install(&resolved_requirements, &venv)
.await
.map_err(|err| Error::RequirementsInstall("setup.py build (install)", err))?;
}
};
build_context
.install(&resolved_requirements, &venv)
.await
.map_err(|err| Error::RequirementsInstall("build-system.requires (install)", err))?;
// If we're using the default backend configuration, skip `get_requires_for_build_*`, since
// we already installed the requirements above.
if let Some(pep517_backend) = &pep517_backend {
create_pep517_build_environment(
&source_tree,
&venv,
pep517_backend,
build_context,
&package_id,
build_kind,
)
.await?;
} else if !source_tree.join("setup.py").is_file() {
return Err(Error::InvalidSourceDist(
"The archive contains neither a `pyproject.toml` nor a `setup.py` file at the top level"
.to_string(),
));
if pep517_backend != &default_backend {
create_pep517_build_environment(
&source_tree,
&venv,
pep517_backend,
build_context,
&package_id,
build_kind,
)
.await?;
}
}
Ok(Self {
@@ -390,12 +420,11 @@ impl SourceBuild {
/// Try calling `prepare_metadata_for_build_wheel` to get the metadata without executing the
/// actual build.
pub async fn get_metadata_without_build(&mut self) -> Result<Option<PathBuf>, Error> {
// setup.py builds don't support this.
let Some(pep517_backend) = &self.pep517_backend else {
return Ok(None);
};
// We've already called this method, but return the existing result is easier than erroring
// We've already called this method; return the existing result.
if let Some(metadata_dir) = &self.metadata_directory {
return Ok(Some(metadata_dir.clone()));
}
@@ -503,7 +532,7 @@ impl SourceBuild {
));
}
let dist = fs::read_dir(self.source_tree.join("dist"))?;
let dist_dir = dist.collect::<io::Result<Vec<DirEntry>>>()?;
let dist_dir = dist.collect::<io::Result<Vec<fs_err::DirEntry>>>()?;
let [dist_wheel] = dist_dir.as_slice() else {
return Err(Error::from_command_output(
format!(
@@ -622,8 +651,8 @@ async fn create_pep517_build_environment(
"#, pep517_backend.backend_import(), build_kind
};
let span = info_span!(
"get_requires_for_build_wheel",
script="build_wheel",
"run_python_script",
script=format!("get_requires_for_build_{}", build_kind),
python_version = %venv.interpreter().version()
);
let output = run_python_script(venv, &script, source_tree)
@@ -644,6 +673,7 @@ async fn create_pep517_build_environment(
.map_err(|err| err.to_string())
.and_then(|last_line| last_line.ok_or("Missing message".to_string()))
.and_then(|message| serde_json::from_str(&message).map_err(|err| err.to_string()));
let extra_requires: Vec<Requirement> = extra_requires.map_err(|err| {
Error::from_command_output(
format!(
@@ -653,14 +683,14 @@ async fn create_pep517_build_environment(
package_id,
)
})?;
// Some packages (such as tqdm 4.66.1) list only extra requires that have already been part of
// the pyproject.toml requires (in this case, `wheel`). We can skip doing the whole resolution
// and installation again.
// TODO(konstin): Do we still need this when we have a fast resolver?
if !extra_requires.is_empty()
&& !extra_requires
.iter()
.all(|req| pep517_backend.requirements.contains(req))
if extra_requires
.iter()
.any(|req| !pep517_backend.requirements.contains(req))
{
debug!("Installing extra requirements for build backend");
let requirements: Vec<Requirement> = pep517_backend
@@ -679,6 +709,7 @@ async fn create_pep517_build_environment(
.await
.map_err(|err| Error::RequirementsInstall("build-system.requires (install)", err))?;
}
Ok(())
}
@@ -23,6 +23,7 @@ use puffin_installer::Downloader;
use puffin_interpreter::{Interpreter, PythonVersion};
use puffin_normalize::ExtraName;
use puffin_resolver::{Manifest, PreReleaseMode, ResolutionMode, ResolutionOptions, Resolver};
use puffin_traits::SetupPyStrategy;
use requirements_txt::EditableRequirement;
use crate::commands::reporters::{DownloadReporter, ResolverReporter};
@@ -44,6 +45,7 @@ pub(crate) async fn pip_compile(
prerelease_mode: PreReleaseMode,
upgrade_mode: UpgradeMode,
index_urls: IndexUrls,
setup_py: SetupPyStrategy,
no_build: bool,
python_version: Option<PythonVersion>,
exclude_newer: Option<DateTime<Utc>>,
@@ -141,6 +143,7 @@ pub(crate) async fn pip_compile(
&interpreter,
&index_urls,
interpreter.sys_executable().to_path_buf(),
setup_py,
no_build,
)
.with_options(options);
@@ -28,7 +28,7 @@ use puffin_normalize::PackageName;
use puffin_resolver::{
Manifest, PreReleaseMode, ResolutionGraph, ResolutionMode, ResolutionOptions, Resolver,
};
use puffin_traits::OnceMap;
use puffin_traits::{OnceMap, SetupPyStrategy};
use requirements_txt::EditableRequirement;
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
@@ -48,6 +48,7 @@ pub(crate) async fn pip_install(
index_urls: IndexUrls,
reinstall: &Reinstall,
link_mode: LinkMode,
setup_py: SetupPyStrategy,
no_build: bool,
strict: bool,
exclude_newer: Option<DateTime<Utc>>,
@@ -144,6 +145,7 @@ pub(crate) async fn pip_install(
&interpreter,
&index_urls,
venv.python_executable(),
setup_py,
no_build,
)
.with_options(options);
+3 -1
View File
@@ -14,7 +14,7 @@ use puffin_client::{RegistryClient, RegistryClientBuilder};
use puffin_dispatch::BuildDispatch;
use puffin_installer::{Downloader, InstallPlan, Reinstall, ResolvedEditable, SitePackages};
use puffin_interpreter::Virtualenv;
use puffin_traits::OnceMap;
use puffin_traits::{OnceMap, SetupPyStrategy};
use pypi_types::Yanked;
use requirements_txt::EditableRequirement;
@@ -30,6 +30,7 @@ pub(crate) async fn pip_sync(
reinstall: &Reinstall,
link_mode: LinkMode,
index_urls: IndexUrls,
setup_py: SetupPyStrategy,
no_build: bool,
strict: bool,
cache: Cache,
@@ -69,6 +70,7 @@ pub(crate) async fn pip_sync(
venv.interpreter(),
&index_urls,
venv.python_executable(),
setup_py,
no_build,
);
+31
View File
@@ -14,6 +14,7 @@ use puffin_installer::Reinstall;
use puffin_interpreter::PythonVersion;
use puffin_normalize::{ExtraName, PackageName};
use puffin_resolver::{PreReleaseMode, ResolutionMode};
use puffin_traits::SetupPyStrategy;
use requirements::ExtrasSpecification;
use crate::commands::{extra_name_with_clap_error, ExitStatus};
@@ -166,6 +167,11 @@ struct PipCompileArgs {
#[clap(long)]
upgrade: bool,
/// Use legacy `setuptools` behavior when building source distributions without a
/// `pyproject.toml`.
#[clap(long)]
legacy_setup_py: bool,
/// Don't build source distributions.
///
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
@@ -228,6 +234,11 @@ struct PipSyncArgs {
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
no_index: bool,
/// Use legacy `setuptools` behavior when building source distributions without a
/// `pyproject.toml`.
#[clap(long)]
legacy_setup_py: bool,
/// Don't build source distributions.
///
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
@@ -324,6 +335,11 @@ struct PipInstallArgs {
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
no_index: bool,
/// Use legacy `setuptools` behavior when building source distributions without a
/// `pyproject.toml`.
#[clap(long)]
legacy_setup_py: bool,
/// Don't build source distributions.
///
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
@@ -480,6 +496,11 @@ async fn inner() -> Result<ExitStatus> {
args.prerelease,
args.upgrade.into(),
index_urls,
if args.legacy_setup_py {
SetupPyStrategy::Setuptools
} else {
SetupPyStrategy::Pep517
},
args.no_build,
args.python_version,
args.exclude_newer,
@@ -502,6 +523,11 @@ async fn inner() -> Result<ExitStatus> {
&reinstall,
args.link_mode,
index_urls,
if args.legacy_setup_py {
SetupPyStrategy::Setuptools
} else {
SetupPyStrategy::Pep517
},
args.no_build,
args.strict,
cache,
@@ -547,6 +573,11 @@ async fn inner() -> Result<ExitStatus> {
index_urls,
&reinstall,
args.link_mode,
if args.legacy_setup_py {
SetupPyStrategy::Setuptools
} else {
SetupPyStrategy::Pep517
},
args.no_build,
args.strict,
args.exclude_newer,
+87
View File
@@ -2813,3 +2813,90 @@ fn trailing_slash() -> Result<()> {
Ok(())
}
/// Resolve a project without a `pyproject.toml`, using the PEP 517 build backend (default).
#[test]
fn compile_legacy_sdist_pep_517() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_dir = TempDir::new()?;
let venv = create_venv_py312(&temp_dir, &cache_dir);
let requirements_in = temp_dir.child("requirements.in");
requirements_in.write_str("flake8 @ https://files.pythonhosted.org/packages/66/53/3ad4a3b74d609b3b9008a10075c40e7c8909eae60af53623c3888f7a529a/flake8-6.0.0.tar.gz")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-compile")
.arg("requirements.in")
.arg("--cache-dir")
.arg(cache_dir.path())
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by Puffin v0.0.1 via the following command:
# puffin pip-compile requirements.in --cache-dir [CACHE_DIR]
flake8 @ https://files.pythonhosted.org/packages/66/53/3ad4a3b74d609b3b9008a10075c40e7c8909eae60af53623c3888f7a529a/flake8-6.0.0.tar.gz
mccabe==0.7.0
# via flake8
pycodestyle==2.10.0
# via flake8
pyflakes==3.0.1
# via flake8
----- stderr -----
Resolved 4 packages in [TIME]
"###);
});
Ok(())
}
/// Resolve a project without a `pyproject.toml`, using `setuptools` directly.
#[test]
fn compile_legacy_sdist_setuptools() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_dir = TempDir::new()?;
let venv = create_venv_py312(&temp_dir, &cache_dir);
let requirements_in = temp_dir.child("requirements.in");
requirements_in.write_str("flake8 @ https://files.pythonhosted.org/packages/66/53/3ad4a3b74d609b3b9008a10075c40e7c8909eae60af53623c3888f7a529a/flake8-6.0.0.tar.gz")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-compile")
.arg("requirements.in")
.arg("--legacy-setup-py")
.arg("--cache-dir")
.arg(cache_dir.path())
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by Puffin v0.0.1 via the following command:
# puffin pip-compile requirements.in --legacy-setup-py --cache-dir [CACHE_DIR]
flake8 @ https://files.pythonhosted.org/packages/66/53/3ad4a3b74d609b3b9008a10075c40e7c8909eae60af53623c3888f7a529a/flake8-6.0.0.tar.gz
mccabe==0.7.0
# via flake8
pycodestyle==2.10.0
# via flake8
pyflakes==3.0.1
# via flake8
----- stderr -----
Resolved 4 packages in [TIME]
"###);
});
Ok(())
}
+71
View File
@@ -2591,3 +2591,74 @@ fn incompatible_wheel() -> Result<()> {
Ok(())
}
/// Install a project without a `pyproject.toml`, using the PEP 517 build backend (default).
#[test]
fn sync_legacy_sdist_pep_517() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let cache_dir = assert_fs::TempDir::new()?;
let venv = create_venv_py312(&temp_dir, &cache_dir);
let requirements_in = temp_dir.child("requirements.in");
requirements_in.write_str("flake8 @ https://files.pythonhosted.org/packages/66/53/3ad4a3b74d609b3b9008a10075c40e7c8909eae60af53623c3888f7a529a/flake8-6.0.0.tar.gz")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.in")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ flake8==6.0.0 (from https://files.pythonhosted.org/packages/66/53/3ad4a3b74d609b3b9008a10075c40e7c8909eae60af53623c3888f7a529a/flake8-6.0.0.tar.gz)
"###);
});
Ok(())
}
/// Install a project without a `pyproject.toml`, using `setuptools` directly.
#[test]
fn sync_legacy_sdist_setuptools() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let cache_dir = assert_fs::TempDir::new()?;
let venv = create_venv_py312(&temp_dir, &cache_dir);
let requirements_in = temp_dir.child("requirements.in");
requirements_in.write_str("flake8 @ https://files.pythonhosted.org/packages/66/53/3ad4a3b74d609b3b9008a10075c40e7c8909eae60af53623c3888f7a529a/flake8-6.0.0.tar.gz")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec()
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("pip-sync")
.arg("requirements.in")
.arg("--legacy-setup-py")
.arg("--cache-dir")
.arg(cache_dir.path())
.env("VIRTUAL_ENV", venv.as_os_str())
.current_dir(&temp_dir), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ flake8==6.0.0 (from https://files.pythonhosted.org/packages/66/53/3ad4a3b74d609b3b9008a10075c40e7c8909eae60af53623c3888f7a529a/flake8-6.0.0.tar.gz)
"###);
});
Ok(())
}
+4 -1
View File
@@ -12,7 +12,7 @@ use puffin_cache::{Cache, CacheArgs};
use puffin_client::RegistryClientBuilder;
use puffin_dispatch::BuildDispatch;
use puffin_interpreter::Virtualenv;
use puffin_traits::{BuildContext, BuildKind};
use puffin_traits::{BuildContext, BuildKind, SetupPyStrategy};
#[derive(Parser)]
pub(crate) struct BuildArgs {
@@ -55,6 +55,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
let venv = Virtualenv::from_env(platform, &cache)?;
let client = RegistryClientBuilder::new(cache.clone()).build();
let index_urls = IndexUrls::default();
let setup_py = SetupPyStrategy::default();
let build_dispatch = BuildDispatch::new(
&client,
@@ -62,6 +63,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
venv.interpreter(),
&index_urls,
venv.python_executable(),
setup_py,
false,
);
@@ -72,6 +74,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
&build_dispatch,
SourceBuildContext::default(),
args.sdist.display().to_string(),
setup_py,
build_kind,
)
.await?;
+4 -1
View File
@@ -25,7 +25,7 @@ use puffin_installer::Downloader;
use puffin_interpreter::Virtualenv;
use puffin_normalize::PackageName;
use puffin_resolver::DistFinder;
use puffin_traits::{BuildContext, OnceMap};
use puffin_traits::{BuildContext, OnceMap, SetupPyStrategy};
#[derive(Parser)]
pub(crate) struct InstallManyArgs {
@@ -60,13 +60,16 @@ pub(crate) async fn install_many(args: InstallManyArgs) -> Result<()> {
let venv = Virtualenv::from_env(platform, &cache)?;
let client = RegistryClientBuilder::new(cache.clone()).build();
let index_urls = IndexUrls::default();
let setup_py = SetupPyStrategy::default();
let tags = venv.interpreter().tags()?;
let build_dispatch = BuildDispatch::new(
&client,
&cache,
venv.interpreter(),
&index_urls,
venv.python_executable(),
setup_py,
args.no_build,
);
+3
View File
@@ -17,6 +17,7 @@ use puffin_client::RegistryClientBuilder;
use puffin_dispatch::BuildDispatch;
use puffin_interpreter::Virtualenv;
use puffin_resolver::{Manifest, ResolutionOptions, Resolver};
use puffin_traits::SetupPyStrategy;
#[derive(ValueEnum, Default, Clone)]
pub(crate) enum ResolveCliFormat {
@@ -50,6 +51,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> {
let venv = Virtualenv::from_env(platform, &cache)?;
let client = RegistryClientBuilder::new(cache.clone()).build();
let index_urls = IndexUrls::default();
let setup_py = SetupPyStrategy::default();
let build_dispatch = BuildDispatch::new(
&client,
@@ -57,6 +59,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> {
venv.interpreter(),
&index_urls,
venv.python_executable(),
setup_py,
args.no_build,
);
+3 -1
View File
@@ -20,7 +20,7 @@ use puffin_client::{RegistryClient, RegistryClientBuilder};
use puffin_dispatch::BuildDispatch;
use puffin_interpreter::Virtualenv;
use puffin_normalize::PackageName;
use puffin_traits::BuildContext;
use puffin_traits::{BuildContext, SetupPyStrategy};
#[derive(Parser)]
pub(crate) struct ResolveManyArgs {
@@ -74,6 +74,7 @@ pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> {
let venv = Virtualenv::from_env(platform, &cache)?;
let client = RegistryClientBuilder::new(cache.clone()).build();
let index_urls = IndexUrls::default();
let setup_py = SetupPyStrategy::default();
let build_dispatch = BuildDispatch::new(
&client,
@@ -81,6 +82,7 @@ pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> {
venv.interpreter(),
&index_urls,
venv.python_executable(),
setup_py,
args.no_build,
);
let build_dispatch = Arc::new(build_dispatch);
+9 -1
View File
@@ -17,7 +17,7 @@ use puffin_client::RegistryClient;
use puffin_installer::{Downloader, InstallPlan, Installer, Reinstall, SitePackages};
use puffin_interpreter::{Interpreter, Virtualenv};
use puffin_resolver::{Manifest, ResolutionOptions, Resolver};
use puffin_traits::{BuildContext, BuildKind, OnceMap};
use puffin_traits::{BuildContext, BuildKind, OnceMap, SetupPyStrategy};
/// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`]
/// documentation.
@@ -27,6 +27,7 @@ pub struct BuildDispatch<'a> {
interpreter: &'a Interpreter,
index_urls: &'a IndexUrls,
base_python: PathBuf,
setup_py: SetupPyStrategy,
no_build: bool,
source_build_context: SourceBuildContext,
options: ResolutionOptions,
@@ -40,6 +41,7 @@ impl<'a> BuildDispatch<'a> {
interpreter: &'a Interpreter,
index_urls: &'a IndexUrls,
base_python: PathBuf,
setup_py: SetupPyStrategy,
no_build: bool,
) -> Self {
Self {
@@ -48,6 +50,7 @@ impl<'a> BuildDispatch<'a> {
interpreter,
index_urls,
base_python,
setup_py,
no_build,
source_build_context: SourceBuildContext::default(),
options: ResolutionOptions::default(),
@@ -81,6 +84,10 @@ impl<'a> BuildContext for BuildDispatch<'a> {
self.no_build
}
fn setup_py_strategy(&self) -> SetupPyStrategy {
self.setup_py
}
async fn resolve<'data>(&'data self, requirements: &'data [Requirement]) -> Result<Resolution> {
let markers = self.interpreter.markers();
let tags = self.interpreter.tags()?;
@@ -232,6 +239,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
self,
self.source_build_context.clone(),
package_id.to_string(),
self.setup_py,
build_kind,
)
.await?;
+9 -1
View File
@@ -20,7 +20,7 @@ use puffin_interpreter::{Interpreter, Virtualenv};
use puffin_resolver::{
Manifest, PreReleaseMode, ResolutionGraph, ResolutionMode, ResolutionOptions, Resolver,
};
use puffin_traits::{BuildContext, BuildKind, SourceBuildTrait};
use puffin_traits::{BuildContext, BuildKind, SetupPyStrategy, SourceBuildTrait};
// Exclude any packages uploaded after this date.
static EXCLUDE_NEWER: Lazy<DateTime<Utc>> = Lazy::new(|| {
@@ -49,6 +49,14 @@ impl BuildContext for DummyContext {
panic!("The test should not need to build source distributions")
}
fn no_build(&self) -> bool {
false
}
fn setup_py_strategy(&self) -> SetupPyStrategy {
SetupPyStrategy::default()
}
async fn resolve<'a>(&'a self, _requirements: &'a [Requirement]) -> Result<Resolution> {
panic!("The test should not need to build source distributions")
}
+14 -3
View File
@@ -68,9 +68,10 @@ pub trait BuildContext {
/// Whether source distribution building is disabled. This [`BuildContext::setup_build`] calls
/// will fail in this case. This method exists to avoid fetching source distributions if we know
/// we can't build them
fn no_build(&self) -> bool {
false
}
fn no_build(&self) -> bool;
/// The strategy to use when building source distributions that lack a `pyproject.toml`.
fn setup_py_strategy(&self) -> SetupPyStrategy;
/// Resolve the given requirements into a ready-to-install set of package versions.
fn resolve<'a>(
@@ -123,6 +124,16 @@ pub trait SourceBuildTrait {
-> impl Future<Output = Result<String>> + Send + 'a;
}
/// The strategy to use when building source distributions that lack a `pyproject.toml`.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum SetupPyStrategy {
/// Perform a PEP 517 build.
#[default]
Pep517,
/// Perform a build by invoking `setuptools` directly.
Setuptools,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum BuildKind {
/// A regular PEP 517 wheel build