Skip to content

Commit 20c1272

Browse files
authored
Stop installing setuptools and wheel (#243)
Currently the buildpack performs a system site-packages install of not only pip, but also setuptools and wheel. This has historically been necessary for pip to be able to build source distributions (sdists) for packages that don't ship with compatible wheels. However: - Thanks to PEP 518, packages can now (and many already do) specify an explicit build backend using `[build-system]` in their `pyproject.toml`. The dependencies specified in that config (such as setuptools and wheel) will be installed by pip into an isolated and ephemeral build environment as part of the source distribution build process. Such packages therefore don't need/use globally installed setuptools/wheel versions. - As of pip v22.1, pip will now default to the isolated build environment mode (along with a fallback legacy setuptools build backend), if the setuptools package isn't installed globally. This means that packages that haven't yet migrated to a PEP 518 `pyproject.toml` build backend config can still build even if setuptools isn't installed globally. There are a small number of rarely used packages in the wild that aren't compatible with build isolation mode, however, these typically require more build dependencies than just setuptools, which means they wouldn't have worked with this buildpack anyway. As such, it's no longer necessary for us to install setuptools and wheel globally. This matches the behaviour of the `venv` and `ensurepip` modules in Python 3.12+, where setuptools and wheel installation has also been removed. And it also matches the default behaviour of Poetry too, whose `install --sync` command removes any implicitly installed packages in the current environment (other than pip). See: https://peps.python.org/pep-0518/ https://pip.pypa.io/en/stable/reference/build-system/ https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/#build-isolation pypa/pip#10717 python/cpython#101039 pypa/get-pip#218 astral-sh/uv#2252 GUS-W-16437776.
1 parent 1ee3732 commit 20c1272

10 files changed

+52
-162
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Removed
11+
12+
- Stopped explicitly installing setuptools and wheel. They will be automatically installed by pip into an isolated build environment if they are required for building a package. ([#243](https://github.com/heroku/buildpacks-python/pull/243))
13+
1014
## [0.13.0] - 2024-08-01
1115

1216
### Changed

Diff for: requirements/setuptools.txt

-1
This file was deleted.

Diff for: requirements/wheel.txt

-1
This file was deleted.

Diff for: src/errors.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,13 @@ fn on_python_layer_error(error: PythonLayerError) {
129129
PythonLayerError::BootstrapPipCommand(error) => match error {
130130
StreamedCommandError::Io(io_error) => log_io_error(
131131
"Unable to bootstrap pip",
132-
"running the command to install pip, setuptools and wheel",
132+
"running the command to install pip",
133133
&io_error,
134134
),
135135
StreamedCommandError::NonZeroExitStatus(exit_status) => log_error(
136136
"Unable to bootstrap pip",
137137
formatdoc! {"
138-
The command to install pip, setuptools and wheel did not exit successfully ({exit_status}).
138+
The command to install pip did not exit successfully ({exit_status}).
139139
140140
See the log output above for more information.
141141

Diff for: src/layers/pip_cache.rs

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::packaging_tool_versions::PackagingToolVersions;
1+
use crate::packaging_tool_versions::PIP_VERSION;
22
use crate::python_version::PythonVersion;
33
use crate::{BuildpackError, PythonBuildpack};
44
use libcnb::build::BuildContext;
@@ -17,14 +17,13 @@ pub(crate) fn prepare_pip_cache(
1717
context: &BuildContext<PythonBuildpack>,
1818
env: &mut Env,
1919
python_version: &PythonVersion,
20-
packaging_tool_versions: &PackagingToolVersions,
2120
) -> Result<(), libcnb::Error<BuildpackError>> {
2221
let new_metadata = PipCacheLayerMetadata {
2322
arch: context.target.arch.clone(),
2423
distro_name: context.target.distro_name.clone(),
2524
distro_version: context.target.distro_version.clone(),
2625
python_version: python_version.to_string(),
27-
packaging_tool_versions: packaging_tool_versions.clone(),
26+
pip_version: PIP_VERSION.to_string(),
2827
};
2928

3029
let layer = context.cached_layer(
@@ -74,15 +73,15 @@ pub(crate) fn prepare_pip_cache(
7473
Ok(())
7574
}
7675

77-
// Timestamp based cache invalidation isn't used here since the Python/pip/setuptools/wheel
78-
// versions will change often enough that it isn't worth the added complexity. Ideally pip
79-
// would support cleaning up its own cache: https://github.com/pypa/pip/issues/6956
76+
// Timestamp based cache invalidation isn't used here since the Python and pip versions will
77+
// change often enough that it isn't worth the added complexity. Ideally pip would support
78+
// cleaning up its own cache: https://github.com/pypa/pip/issues/6956
8079
#[derive(Deserialize, PartialEq, Serialize)]
8180
#[serde(deny_unknown_fields)]
8281
struct PipCacheLayerMetadata {
8382
arch: String,
8483
distro_name: String,
8584
distro_version: String,
8685
python_version: String,
87-
packaging_tool_versions: PackagingToolVersions,
86+
pip_version: String,
8887
}

Diff for: src/layers/python.rs

+11-55
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::packaging_tool_versions::PackagingToolVersions;
1+
use crate::packaging_tool_versions::PIP_VERSION;
22
use crate::python_version::PythonVersion;
33
use crate::utils::{self, DownloadUnpackArchiveError, StreamedCommandError};
44
use crate::{BuildpackError, PythonBuildpack};
@@ -17,7 +17,7 @@ use std::path::{Path, PathBuf};
1717
use std::process::Command;
1818
use std::{fs, io};
1919

20-
/// Creates a layer containing the Python runtime and the packages `pip`, `setuptools` and `wheel`.
20+
/// Creates a layer containing the Python runtime and pip.
2121
//
2222
// We install both Python and the packaging tools into the same layer, since:
2323
// - We don't want to mix buildpack/packaging dependencies with the app's own dependencies
@@ -31,25 +31,18 @@ use std::{fs, io};
3131
// - This leaves just the system site-packages directory, which exists within the Python
3232
// installation directory and Python does not support moving it elsewhere.
3333
// - It matches what both local and official Docker image environments do.
34-
#[allow(clippy::too_many_lines)]
3534
pub(crate) fn install_python_and_packaging_tools(
3635
context: &BuildContext<PythonBuildpack>,
3736
env: &mut Env,
3837
python_version: &PythonVersion,
39-
packaging_tool_versions: &PackagingToolVersions,
4038
) -> Result<(), libcnb::Error<BuildpackError>> {
4139
let new_metadata = PythonLayerMetadata {
4240
arch: context.target.arch.clone(),
4341
distro_name: context.target.distro_name.clone(),
4442
distro_version: context.target.distro_version.clone(),
4543
python_version: python_version.to_string(),
46-
packaging_tool_versions: packaging_tool_versions.clone(),
44+
pip_version: PIP_VERSION.to_string(),
4745
};
48-
let PackagingToolVersions {
49-
pip_version,
50-
setuptools_version,
51-
wheel_version,
52-
} = packaging_tool_versions;
5346

5447
let layer = context.cached_layer(
5548
layer_name!("python"),
@@ -71,9 +64,8 @@ pub(crate) fn install_python_and_packaging_tools(
7164

7265
match layer.state {
7366
LayerState::Restored { .. } => {
74-
log_info(format!("Using cached Python {python_version}"));
7567
log_info(format!(
76-
"Using cached pip {pip_version}, setuptools {setuptools_version} and wheel {wheel_version}"
68+
"Using cached Python {python_version} and pip {PIP_VERSION}"
7769
));
7870
}
7971
LayerState::Empty { ref cause } => {
@@ -117,9 +109,7 @@ pub(crate) fn install_python_and_packaging_tools(
117109
return Ok(());
118110
}
119111

120-
log_info(format!(
121-
"Installing pip {pip_version}, setuptools {setuptools_version} and wheel {wheel_version}"
122-
));
112+
log_info(format!("Installing pip {PIP_VERSION}"));
123113

124114
let python_stdlib_dir = layer_path.join(format!(
125115
"lib/python{}.{}",
@@ -140,9 +130,7 @@ pub(crate) fn install_python_and_packaging_tools(
140130
"--no-cache-dir",
141131
"--no-input",
142132
"--quiet",
143-
format!("pip=={pip_version}").as_str(),
144-
format!("setuptools=={setuptools_version}").as_str(),
145-
format!("wheel=={wheel_version}").as_str(),
133+
format!("pip=={PIP_VERSION}").as_str(),
146134
])
147135
.current_dir(&context.app_dir)
148136
.env_clear()
@@ -170,7 +158,7 @@ struct PythonLayerMetadata {
170158
distro_name: String,
171159
distro_version: String,
172160
python_version: String,
173-
packaging_tool_versions: PackagingToolVersions,
161+
pip_version: String,
174162
}
175163

176164
/// Compare cached layer metadata to the new layer metadata to determine if the cache should be
@@ -189,25 +177,15 @@ fn cache_invalidation_reasons(
189177
distro_name: cached_distro_name,
190178
distro_version: cached_distro_version,
191179
python_version: cached_python_version,
192-
packaging_tool_versions:
193-
PackagingToolVersions {
194-
pip_version: cached_pip_version,
195-
setuptools_version: cached_setuptools_version,
196-
wheel_version: cached_wheel_version,
197-
},
180+
pip_version: cached_pip_version,
198181
} = cached_metadata;
199182

200183
let PythonLayerMetadata {
201184
arch,
202185
distro_name,
203186
distro_version,
204187
python_version,
205-
packaging_tool_versions:
206-
PackagingToolVersions {
207-
pip_version,
208-
setuptools_version,
209-
wheel_version,
210-
},
188+
pip_version,
211189
} = new_metadata;
212190

213191
let mut reasons = Vec::new();
@@ -236,18 +214,6 @@ fn cache_invalidation_reasons(
236214
));
237215
}
238216

239-
if cached_setuptools_version != setuptools_version {
240-
reasons.push(format!(
241-
"The setuptools version has changed from {cached_setuptools_version} to {setuptools_version}"
242-
));
243-
}
244-
245-
if cached_wheel_version != wheel_version {
246-
reasons.push(format!(
247-
"The wheel version has changed from {cached_wheel_version} to {wheel_version}"
248-
));
249-
}
250-
251217
reasons
252218
}
253219

@@ -423,11 +389,7 @@ mod tests {
423389
distro_name: "ubuntu".to_string(),
424390
distro_version: "22.04".to_string(),
425391
python_version: "3.11.0".to_string(),
426-
packaging_tool_versions: PackagingToolVersions {
427-
pip_version: "A.B.C".to_string(),
428-
setuptools_version: "D.E.F".to_string(),
429-
wheel_version: "G.H.I".to_string(),
430-
},
392+
pip_version: "A.B.C".to_string(),
431393
}
432394
}
433395

@@ -462,11 +424,7 @@ mod tests {
462424
distro_name: "debian".to_string(),
463425
distro_version: "12".to_string(),
464426
python_version: "3.11.1".to_string(),
465-
packaging_tool_versions: PackagingToolVersions {
466-
pip_version: "A.B.C-new".to_string(),
467-
setuptools_version: "D.E.F-new".to_string(),
468-
wheel_version: "G.H.I-new".to_string(),
469-
},
427+
pip_version: "A.B.C-new".to_string(),
470428
};
471429
assert_eq!(
472430
cache_invalidation_reasons(&cached_metadata, &new_metadata),
@@ -475,8 +433,6 @@ mod tests {
475433
"The OS has changed from ubuntu-22.04 to debian-12",
476434
"The Python version has changed from 3.11.0 to 3.11.1",
477435
"The pip version has changed from A.B.C to A.B.C-new",
478-
"The setuptools version has changed from D.E.F to D.E.F-new",
479-
"The wheel version has changed from G.H.I to G.H.I-new"
480436
]
481437
);
482438
}

Diff for: src/main.rs

+4-16
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ use crate::layers::pip_dependencies::PipDependenciesLayerError;
1313
use crate::layers::python::{self, PythonLayerError};
1414
use crate::layers::{pip_cache, pip_dependencies};
1515
use crate::package_manager::{DeterminePackageManagerError, PackageManager};
16-
use crate::packaging_tool_versions::PackagingToolVersions;
1716
use crate::python_version::PythonVersionError;
1817
use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder};
1918
use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder};
@@ -53,7 +52,6 @@ impl Buildpack for PythonBuildpack {
5352
log_header("Determining Python version");
5453
let python_version = python_version::determine_python_version(&context.app_dir)
5554
.map_err(BuildpackError::PythonVersion)?;
56-
let packaging_tool_versions = PackagingToolVersions::default();
5755

5856
// We inherit the current process's env vars, since we want `PATH` and `HOME` from the OS
5957
// to be set (so that later commands can find tools like Git in the base image), along
@@ -62,26 +60,16 @@ impl Buildpack for PythonBuildpack {
6260
// making sure that buildpack env vars take precedence in layers envs and command usage.
6361
let mut env = Env::from_current();
6462

65-
// Create the layer containing the Python runtime, and the packages `pip`, `setuptools` and `wheel`.
66-
log_header("Installing Python and packaging tools");
67-
python::install_python_and_packaging_tools(
68-
&context,
69-
&mut env,
70-
&python_version,
71-
&packaging_tool_versions,
72-
)?;
63+
// Create the layer containing the Python runtime and pip.
64+
log_header("Installing Python and pip");
65+
python::install_python_and_packaging_tools(&context, &mut env, &python_version)?;
7366

7467
// Create the layers for the application dependencies and package manager cache.
7568
// In the future support will be added for package managers other than pip.
7669
let dependencies_layer_dir = match package_manager {
7770
PackageManager::Pip => {
7871
log_header("Installing dependencies using pip");
79-
pip_cache::prepare_pip_cache(
80-
&context,
81-
&mut env,
82-
&python_version,
83-
&packaging_tool_versions,
84-
)?;
72+
pip_cache::prepare_pip_cache(&context, &mut env, &python_version)?;
8573
pip_dependencies::install_dependencies(&context, &mut env)?
8674
}
8775
};

Diff for: src/packaging_tool_versions.rs

+2-28
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,10 @@
1-
use serde::{Deserialize, Serialize};
21
use std::str;
32

43
// We store these versions in requirements files so that Dependabot can update them.
54
// Each file must contain a single package specifier in the format `package==1.2.3`,
65
// from which we extract/validate the version substring at compile time.
7-
const PIP_VERSION: &str = extract_requirement_version(include_str!("../requirements/pip.txt"));
8-
const SETUPTOOLS_VERSION: &str =
9-
extract_requirement_version(include_str!("../requirements/setuptools.txt"));
10-
const WHEEL_VERSION: &str = extract_requirement_version(include_str!("../requirements/wheel.txt"));
11-
12-
/// The versions of various packaging tools used during the build.
13-
/// These are always installed, and are independent of the chosen package manager.
14-
/// Strings are used instead of a semver version, since these packages don't use
15-
/// semver, and we never introspect the version parts anyway.
16-
#[allow(clippy::struct_field_names)]
17-
#[derive(Clone, Deserialize, PartialEq, Serialize)]
18-
#[serde(deny_unknown_fields)]
19-
pub(crate) struct PackagingToolVersions {
20-
pub(crate) pip_version: String,
21-
pub(crate) setuptools_version: String,
22-
pub(crate) wheel_version: String,
23-
}
24-
25-
impl Default for PackagingToolVersions {
26-
fn default() -> Self {
27-
Self {
28-
pip_version: PIP_VERSION.to_string(),
29-
setuptools_version: SETUPTOOLS_VERSION.to_string(),
30-
wheel_version: WHEEL_VERSION.to_string(),
31-
}
32-
}
33-
}
6+
pub(crate) const PIP_VERSION: &str =
7+
extract_requirement_version(include_str!("../requirements/pip.txt"));
348

359
// Extract the version substring from an exact-version package specifier (such as `foo==1.2.3`).
3610
// This function should only be used to extract the version constants from the buildpack's own

0 commit comments

Comments
 (0)